Migrate from window.electron to tauri IPC functions across components
- Replaced `window.electron.invoke` calls with equivalent `tauri` function calls for all IPC interactions. - Removed `electron.d.ts` TypeScript definitions as they are no longer needed. - Updated related logic for offline/online state synchronization. - Added `types.rs` and `shared/mod.rs` modules to support Tauri IPC integration with Rust enums and shared logic. - Refactored IPC request queues to use updated handler names for consistency with Tauri.
This commit is contained in:
@@ -10,6 +10,7 @@ import {SessionProps} from "@/lib/models/Session";
|
||||
import System from "@/lib/models/System";
|
||||
import {SessionContext} from "@/context/SessionContext";
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
const messagesMap = {
|
||||
fr: frMessages,
|
||||
@@ -36,10 +37,7 @@ export default function LoginWrapper({children}: { children: React.ReactNode })
|
||||
|
||||
useEffect((): void => {
|
||||
if (session.isConnected) {
|
||||
// Pas de router.push dans Electron, le main process gère
|
||||
if (!window.electron) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
tauri.loginSuccess();
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
|
||||
@@ -2,17 +2,16 @@ import {useContext, useState} from "react";
|
||||
import System from "@/lib/models/System";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faEnvelope, faLock} from "@fortawesome/free-solid-svg-icons";
|
||||
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
export default function LoginForm() {
|
||||
const {errorMessage} = useContext(AlertContext);
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const {setSession} = useContext<SessionContextProps>(SessionContext);
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext)
|
||||
|
||||
@@ -48,21 +47,8 @@ export default function LoginForm() {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
// Stocker le token dans electron-store via IPC
|
||||
if (window.electron) {
|
||||
await window.electron.setToken(response);
|
||||
window.electron.loginSuccess(response);
|
||||
} else {
|
||||
// Fallback pour le mode dev web
|
||||
System.setCookie('token', response, 30);
|
||||
const token: string | null = System.getCookie('token');
|
||||
if (!token) {
|
||||
errorMessage(t('loginForm.error.connection'));
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
setSession({isConnected: true, user: null, accessToken: token})
|
||||
}
|
||||
await tauri.setToken(response);
|
||||
await tauri.loginSuccess();
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(t('loginForm.error.server'));
|
||||
|
||||
@@ -4,60 +4,44 @@ import React, {useContext, useEffect} from "react";
|
||||
import System from "@/lib/models/System";
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
import {configs} from "@/lib/configs";
|
||||
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
export default function SocialForm() {
|
||||
const {errorMessage} = useContext(AlertContext);
|
||||
const {setSession} = useContext<SessionContextProps>(SessionContext)
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext)
|
||||
const isElectron = typeof window !== 'undefined' && !!window.electron;
|
||||
|
||||
useEffect((): void => {
|
||||
// Skip URL parsing in Electron (OAuth is handled via BrowserWindow)
|
||||
if (isElectron) return;
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const provider: string | null = params.get('provider');
|
||||
if (!provider) {
|
||||
return;
|
||||
}
|
||||
if (!provider) return;
|
||||
|
||||
const code: string | null = params.get('code');
|
||||
if (!code) {
|
||||
return;
|
||||
}
|
||||
if (!code) return;
|
||||
|
||||
if (provider === 'google') {
|
||||
handleGoogleLogin(code).then();
|
||||
return;
|
||||
}
|
||||
if (provider === 'facebook') {
|
||||
const state: string | null = params.get('state');
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
if (!state) return;
|
||||
handleFacebookLogin(code, state).then();
|
||||
return;
|
||||
}
|
||||
if (provider === 'apple') {
|
||||
const state: string | null = params.get('state');
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
if (!state) return;
|
||||
handleAppleLogin(code, state).then();
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
async function handleLoginSuccess(token: string): Promise<void> {
|
||||
if (window.electron) {
|
||||
await window.electron.setToken(token);
|
||||
window.electron.loginSuccess(token);
|
||||
} else {
|
||||
System.setCookie('token', token, 30);
|
||||
setSession({isConnected: true, user: null, accessToken: token});
|
||||
}
|
||||
await tauri.setToken(token);
|
||||
await tauri.loginSuccess();
|
||||
}
|
||||
|
||||
async function handleFacebookLogin(code: string, state: string): Promise<void> {
|
||||
@@ -102,28 +86,10 @@ export default function SocialForm() {
|
||||
}
|
||||
|
||||
async function handleOAuthClick(provider: 'google' | 'facebook' | 'apple'): Promise<void> {
|
||||
if (!window.electron) return;
|
||||
|
||||
try {
|
||||
const result = await window.electron.oauthLogin(provider, configs.baseUrl);
|
||||
|
||||
if (!result.success) {
|
||||
if (result.error !== 'Window closed by user') {
|
||||
errorMessage(t('socialForm.error.connection'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.code) {
|
||||
if (provider === 'google') {
|
||||
await handleGoogleLogin(result.code);
|
||||
} else if (provider === 'facebook' && result.state) {
|
||||
await handleFacebookLogin(result.code, result.state);
|
||||
} else if (provider === 'apple' && result.state) {
|
||||
await handleAppleLogin(result.code, result.state);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const oauthUrl = `${configs.baseUrl}auth/${provider}/desktop`;
|
||||
await tauri.openExternal(oauthUrl);
|
||||
} catch {
|
||||
errorMessage(t('socialForm.error.connection'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import SocialForm from "@/app/login/login/SocialForm";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext} from "@/context/LangContext";
|
||||
import System from "@/lib/models/System";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
export default function LoginPage() {
|
||||
const t = useTranslations();
|
||||
@@ -22,21 +23,12 @@ export default function LoginPage() {
|
||||
|
||||
useEffect(() => {
|
||||
async function checkFirstConnectionAndNetwork() {
|
||||
// Check if we're in Electron
|
||||
if (!window.electron) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if token exists (first connection)
|
||||
const token = await window.electron.getToken();
|
||||
const token = await tauri.getToken();
|
||||
const hasToken = !!token;
|
||||
|
||||
// Check network status
|
||||
const online = navigator.onLine;
|
||||
setIsOnline(online);
|
||||
|
||||
// Show warning if first connection AND offline
|
||||
if (!hasToken && !online) {
|
||||
setShowOfflineWarning(true);
|
||||
}
|
||||
@@ -47,19 +39,19 @@ export default function LoginPage() {
|
||||
|
||||
checkFirstConnectionAndNetwork();
|
||||
|
||||
// Listen for online/offline events
|
||||
const handleOnline = () => {
|
||||
setIsOnline(true);
|
||||
setShowOfflineWarning(false);
|
||||
};
|
||||
const handleOffline = async () => {
|
||||
setIsOnline(false);
|
||||
// Check if token exists
|
||||
if (window.electron) {
|
||||
const token = await window.electron.getToken();
|
||||
try {
|
||||
const token = await tauri.getToken();
|
||||
if (!token) {
|
||||
setShowOfflineWarning(true);
|
||||
}
|
||||
} catch {
|
||||
setShowOfflineWarning(true);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,46 +5,34 @@ import OfflinePinVerify from '@/components/offline/OfflinePinVerify';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faWifi, faArrowLeft } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
export default function OfflineLoginPage() {
|
||||
const t = useTranslations();
|
||||
|
||||
async function handlePinSuccess(userId: string): Promise<void> {
|
||||
|
||||
// Initialize database with user's encryption key
|
||||
if (window.electron) {
|
||||
try {
|
||||
// Get encryption key
|
||||
const encryptionKey = await window.electron.getUserEncryptionKey(userId);
|
||||
const encryptionKey = await tauri.getUserEncryptionKey(userId);
|
||||
if (encryptionKey) {
|
||||
// Initialize database
|
||||
await window.electron.dbInitialize(userId, encryptionKey);
|
||||
|
||||
// Navigate to main page
|
||||
window.location.href = '/';
|
||||
await tauri.dbInitialize(userId, encryptionKey);
|
||||
await tauri.loginSuccess();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[OfflineLogin] Error initializing database:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleBackToOnline(): void {
|
||||
if (window.electron) {
|
||||
window.electron.logout();
|
||||
}
|
||||
tauri.logout();
|
||||
}
|
||||
|
||||
useEffect((): void => {
|
||||
// Check if we have offline capability
|
||||
async function checkOfflineCapability() {
|
||||
if (window.electron) {
|
||||
const offlineStatus = await window.electron.offlineModeGet();
|
||||
const offlineStatus = await tauri.offlineModeGet();
|
||||
if (!offlineStatus.hasPin) {
|
||||
window.location.href = '/login/login';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkOfflineCapability().then();
|
||||
}, []);
|
||||
|
||||
121
app/page.tsx
121
app/page.tsx
@@ -40,6 +40,7 @@ import {SeriesSyncContext} from "@/context/SeriesSyncContext";
|
||||
import useSyncBooks from "@/hooks/useSyncBooks";
|
||||
import useSyncSeries from "@/hooks/useSyncSeries";
|
||||
import {LocalSyncQueueContext, LocalSyncOperation} from "@/context/SyncQueueContext";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface RemovedItemRecord {
|
||||
removal_id: string;
|
||||
@@ -194,7 +195,7 @@ function ScribeContent() {
|
||||
|
||||
for (const operation of queueCopy) {
|
||||
try {
|
||||
await window.electron.invoke(operation.channel, operation.data);
|
||||
await tauri.invoke(operation.channel, operation.data);
|
||||
setLocalSyncQueue((prev: LocalSyncOperation[]): LocalSyncOperation[] =>
|
||||
prev.filter((op: LocalSyncOperation): boolean => op.id !== operation.id)
|
||||
);
|
||||
@@ -277,7 +278,20 @@ function ScribeContent() {
|
||||
];
|
||||
|
||||
useEffect((): void => {
|
||||
checkAuthentification().then()
|
||||
checkAuthentification().then();
|
||||
|
||||
let unlisten: (() => void) | undefined;
|
||||
import('@tauri-apps/api/event').then(function ({listen}) {
|
||||
listen('auth-success', function () {
|
||||
checkAuthentification().then();
|
||||
}).then(function (fn) {
|
||||
unlisten = fn;
|
||||
});
|
||||
});
|
||||
|
||||
return (): void => {
|
||||
if (unlisten) unlisten();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect((): void => {
|
||||
@@ -373,20 +387,20 @@ function ScribeContent() {
|
||||
|
||||
if (!isCurrentlyOffline()) {
|
||||
if (offlineMode.isDatabaseInitialized) {
|
||||
localBooksResponse = await window.electron.invoke<SyncedBook[]>('db:books:synced');
|
||||
localBooksResponse = await tauri.getSyncedBooks() as SyncedBook[];
|
||||
const lastOnlineStr: string | null = localStorage.getItem('lastOnlineTimestamp');
|
||||
const lastOnlineTimestamp: number = lastOnlineStr ? parseInt(lastOnlineStr, 10) : 0;
|
||||
const localTombstones: RemovedItemRecord[] = await window.electron.invoke<RemovedItemRecord[]>('db:tombstones:since', lastOnlineTimestamp);
|
||||
const localTombstones: RemovedItemRecord[] = await tauri.getTombstonesSince(lastOnlineTimestamp) as RemovedItemRecord[];
|
||||
const serverResponse: SyncedBooksResponse = await System.authPostToServer<SyncedBooksResponse>('books/synced', { lastOnlineTimestamp, tombstones: localTombstones }, session.accessToken, locale);
|
||||
serverBooksResponse = serverResponse.books;
|
||||
await window.electron.invoke<void>('db:tombstones:apply:books', serverResponse.tombstones);
|
||||
await tauri.applyBookTombstones(serverResponse.tombstones);
|
||||
} else {
|
||||
const serverResponse: SyncedBooksResponse = await System.authPostToServer<SyncedBooksResponse>('books/synced', { lastOnlineTimestamp: 0, tombstones: [] }, session.accessToken, locale);
|
||||
serverBooksResponse = serverResponse.books;
|
||||
}
|
||||
} else {
|
||||
if (offlineMode.isDatabaseInitialized) {
|
||||
localBooksResponse = await window.electron.invoke<SyncedBook[]>('db:books:synced');
|
||||
localBooksResponse = await tauri.getSyncedBooks() as SyncedBook[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,20 +422,20 @@ function ScribeContent() {
|
||||
|
||||
if (!isCurrentlyOffline()) {
|
||||
if (offlineMode.isDatabaseInitialized) {
|
||||
localSeriesResponse = await window.electron.invoke<SyncedSeries[]>('db:series:synced');
|
||||
localSeriesResponse = await tauri.getSyncedSeries() as SyncedSeries[];
|
||||
const lastOnlineStr: string | null = localStorage.getItem('lastOnlineTimestamp');
|
||||
const lastOnlineTimestamp: number = lastOnlineStr ? parseInt(lastOnlineStr, 10) : 0;
|
||||
const localTombstones: RemovedItemRecord[] = await window.electron.invoke<RemovedItemRecord[]>('db:tombstones:since', lastOnlineTimestamp);
|
||||
const localTombstones: RemovedItemRecord[] = await tauri.getTombstonesSince(lastOnlineTimestamp) as RemovedItemRecord[];
|
||||
const serverResponse: SyncedSeriesResponse = await System.authPostToServer<SyncedSeriesResponse>('series/synced', { lastOnlineTimestamp, tombstones: localTombstones }, session.accessToken, locale);
|
||||
serverSeriesResponse = serverResponse.series;
|
||||
await window.electron.invoke<void>('db:tombstones:apply:series', serverResponse.tombstones);
|
||||
await tauri.applySeriesTombstones(serverResponse.tombstones);
|
||||
} else {
|
||||
const serverResponse: SyncedSeriesResponse = await System.authPostToServer<SyncedSeriesResponse>('series/synced', { lastOnlineTimestamp: 0, tombstones: [] }, session.accessToken, locale);
|
||||
serverSeriesResponse = serverResponse.series;
|
||||
}
|
||||
} else {
|
||||
if (offlineMode.isDatabaseInitialized) {
|
||||
localSeriesResponse = await window.electron.invoke<SyncedSeries[]>('db:series:synced');
|
||||
localSeriesResponse = await tauri.getSyncedSeries() as SyncedSeries[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,42 +451,16 @@ function ScribeContent() {
|
||||
}
|
||||
|
||||
|
||||
useEffect(():void => {
|
||||
async function checkPinSetup() {
|
||||
if (session.isConnected && window.electron) {
|
||||
try {
|
||||
const offlineStatus = await window.electron.offlineModeGet();
|
||||
|
||||
if (!offlineStatus.hasPin) {
|
||||
setTimeout(():void => {
|
||||
setShowPinSetup(true);
|
||||
}, 2000);
|
||||
}
|
||||
} catch (e:unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage('Unknown error occurred while checking offline mode')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkPinSetup().then();
|
||||
|
||||
}, [session.isConnected]);
|
||||
|
||||
async function handlePinVerifySuccess(userId: string): Promise<void> {
|
||||
try {
|
||||
if (window.electron) {
|
||||
const storedToken: string | null = await window.electron.getToken();
|
||||
const encryptionKey:string|null = await window.electron.getUserEncryptionKey(userId);
|
||||
const storedToken: string | null = await tauri.getToken();
|
||||
const encryptionKey: string | null = await tauri.getUserEncryptionKey(userId);
|
||||
|
||||
if (encryptionKey) {
|
||||
await window.electron.dbInitialize(userId, encryptionKey);
|
||||
await tauri.dbInitialize(userId, encryptionKey);
|
||||
setOfflineMode(prev => ({...prev, isDatabaseInitialized: true}));
|
||||
|
||||
const localUser:UserProps = await window.electron.invoke('db:user:info');
|
||||
const localUser: UserProps = await tauri.getUserInfo();
|
||||
if (localUser && localUser.id) {
|
||||
setSession({
|
||||
isConnected: true,
|
||||
@@ -488,7 +476,6 @@ function ScribeContent() {
|
||||
} else {
|
||||
errorMessage(t("homePage.errors.encryptionKeyError"));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[OfflinePin] Error initializing offline mode:', error);
|
||||
errorMessage(t("homePage.errors.offlineModeError"));
|
||||
@@ -530,12 +517,10 @@ function ScribeContent() {
|
||||
async function checkAuthentification(): Promise<void> {
|
||||
let token: string | null = null;
|
||||
|
||||
if (typeof window !== 'undefined' && window.electron) {
|
||||
try {
|
||||
token = await window.electron.getToken();
|
||||
token = await tauri.getToken();
|
||||
} catch (e) {
|
||||
console.error('Error getting token from electron:', e);
|
||||
}
|
||||
console.error('Error getting token:', e);
|
||||
}
|
||||
|
||||
if (token) {
|
||||
@@ -543,22 +528,20 @@ function ScribeContent() {
|
||||
const user: UserProps = await System.authGetQueryToServer<UserProps>('user/infos', token, locale);
|
||||
if (!user) {
|
||||
errorMessage(t("homePage.errors.userNotFound"));
|
||||
if (window.electron) {
|
||||
await window.electron.removeToken();
|
||||
window.electron.logout();
|
||||
}
|
||||
await tauri.removeToken();
|
||||
tauri.logout();
|
||||
return;
|
||||
}
|
||||
|
||||
if (window.electron && user.id) {
|
||||
if (user.id) {
|
||||
try {
|
||||
const initResult = await window.electron.initUser(user.id);
|
||||
const initResult = await tauri.initUser(user.id);
|
||||
if (!initResult.success) {
|
||||
errorMessage(initResult.error || t("homePage.errors.offlineInitError"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const offlineStatus = await window.electron.offlineModeGet();
|
||||
const offlineStatus = await tauri.offlineModeGet();
|
||||
if (!offlineStatus.hasPin) {
|
||||
setTimeout(():void => {
|
||||
setShowPinSetup(true);
|
||||
@@ -571,12 +554,12 @@ function ScribeContent() {
|
||||
console.error('[Page] Error initializing user:', error);
|
||||
}
|
||||
}
|
||||
if (window.electron && user.id) {
|
||||
if (user.id) {
|
||||
try {
|
||||
const dbInitialized: boolean = await initializeDatabase(user.id);
|
||||
if (dbInitialized) {
|
||||
try {
|
||||
await window.electron.invoke('db:user:sync', {
|
||||
await tauri.syncUser({
|
||||
userId: user.id,
|
||||
firstName: user.name,
|
||||
lastName: user.lastName,
|
||||
@@ -587,7 +570,6 @@ function ScribeContent() {
|
||||
errorMessage(t("homePage.errors.syncError"));
|
||||
}
|
||||
} else {
|
||||
console.error('[Page] Database initialization failed');
|
||||
errorMessage(t("homePage.errors.dbInitError"));
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -599,13 +581,11 @@ function ScribeContent() {
|
||||
user: user,
|
||||
accessToken: token,
|
||||
});
|
||||
console.log(user)
|
||||
setCurrentCredits(user.creditsBalance)
|
||||
setAmountSpent(user.aiUsage)
|
||||
} catch (e: unknown) {
|
||||
if (window.electron) {
|
||||
try {
|
||||
const offlineStatus = await window.electron.offlineModeGet();
|
||||
const offlineStatus = await tauri.offlineModeGet();
|
||||
|
||||
if (offlineStatus.hasPin && offlineStatus.lastUserId) {
|
||||
setOfflineMode((prev:OfflineMode):OfflineMode => ({...prev, isOffline: true, isNetworkOnline: false}));
|
||||
@@ -613,15 +593,12 @@ function ScribeContent() {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
} else {
|
||||
if (window.electron) {
|
||||
await window.electron.removeToken();
|
||||
window.electron.logout();
|
||||
}
|
||||
await tauri.removeToken();
|
||||
tauri.logout();
|
||||
}
|
||||
} catch (offlineError) {
|
||||
errorMessage(t("homePage.errors.offlineError"));
|
||||
}
|
||||
}
|
||||
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
@@ -630,9 +607,8 @@ function ScribeContent() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (window.electron) {
|
||||
try {
|
||||
const offlineStatus = await window.electron.offlineModeGet();
|
||||
const offlineStatus = await tauri.offlineModeGet();
|
||||
|
||||
if (offlineStatus.hasPin && offlineStatus.lastUserId) {
|
||||
setOfflineMode(prev => ({...prev, isOffline: true, isNetworkOnline: false}));
|
||||
@@ -643,8 +619,7 @@ function ScribeContent() {
|
||||
} catch (error) {
|
||||
errorMessage(t("homePage.errors.authenticationError"));
|
||||
}
|
||||
window.electron.logout();
|
||||
}
|
||||
tauri.logout();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -685,14 +660,14 @@ function ScribeContent() {
|
||||
setCurrentChapter(undefined);
|
||||
return;
|
||||
}
|
||||
response = await window.electron.invoke('db:chapter:last', currentBook?.bookId)
|
||||
response = await tauri.getLastChapter(currentBook?.bookId ?? '')
|
||||
} else {
|
||||
if (currentBook?.localBook) {
|
||||
if (!offlineMode.isDatabaseInitialized) {
|
||||
setCurrentChapter(undefined);
|
||||
return;
|
||||
}
|
||||
response = await window.electron.invoke('db:chapter:last', currentBook?.bookId)
|
||||
response = await tauri.getLastChapter(currentBook?.bookId ?? '')
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<ChapterProps | null>(`chapter/last-chapter`, session.accessToken, locale, {bookid: currentBook?.bookId});
|
||||
}
|
||||
@@ -768,7 +743,7 @@ function ScribeContent() {
|
||||
!isTermsAccepted && !isCurrentlyOffline() && <TermsOfUse onAccept={handleTermsAcceptance}/>
|
||||
}
|
||||
{
|
||||
showPinSetup && window.electron && (
|
||||
showPinSetup && (
|
||||
<OfflinePinSetup
|
||||
showOnFirstLogin={true}
|
||||
onClose={():void => setShowPinSetup(false)}
|
||||
@@ -779,12 +754,10 @@ function ScribeContent() {
|
||||
)
|
||||
}
|
||||
{
|
||||
showPinVerify && window.electron && (
|
||||
showPinVerify && (
|
||||
<OfflinePinVerify
|
||||
onSuccess={handlePinVerifySuccess}
|
||||
onCancel={():void => {
|
||||
//window.electron.logout();
|
||||
}}
|
||||
onCancel={():void => {}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {SessionContext} from "@/context/SessionContext";
|
||||
import NoPicture from "@/components/NoPicture";
|
||||
import System from "@/lib/models/System";
|
||||
import {useTranslations} from "next-intl";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
export default function UserMenu() {
|
||||
const {session} = useContext(SessionContext);
|
||||
@@ -34,8 +35,8 @@ export default function UserMenu() {
|
||||
|
||||
async function handleLogout(): Promise<void> {
|
||||
System.removeCookie("token");
|
||||
await window.electron.removeToken();
|
||||
window.electron.logout();
|
||||
await tauri.removeToken();
|
||||
tauri.logout();
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client'
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import {ChangeEvent, Dispatch, RefObject, SetStateAction, useContext, useEffect, useRef, useState} from "react";
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
import System from "@/lib/models/System";
|
||||
@@ -139,7 +140,7 @@ export default function AddNewBookForm({setCloseForm}: { setCloseForm: Dispatch<
|
||||
|
||||
let bookId: string;
|
||||
if (isCurrentlyOffline()) {
|
||||
bookId = await window.electron.invoke<string>('db:book:create', bookData);
|
||||
bookId = await tauri.createBook(bookData);
|
||||
} else {
|
||||
bookId = await System.authPostToServer<string>('book/add', bookData, token, lang);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import {useContext, useEffect, useRef, useState} from "react";
|
||||
import System from "@/lib/models/System";
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
@@ -178,11 +179,11 @@ export default function BookList() {
|
||||
const [onlineBooks, localBooks, onlineSeries, localSeries] = await Promise.all([
|
||||
System.authGetQueryToServer<BookProps[]>('books', accessToken, lang),
|
||||
offlineMode.isDatabaseInitialized
|
||||
? window.electron.invoke<BookProps[]>('db:book:books')
|
||||
? tauri.getBooks()
|
||||
: Promise.resolve([]),
|
||||
System.authGetQueryToServer<SeriesListItemProps[]>('series/list', accessToken, lang),
|
||||
offlineMode.isDatabaseInitialized
|
||||
? window.electron.invoke<SeriesListItemProps[]>('db:series:list')
|
||||
? tauri.getSeriesList() as Promise<SeriesListItemProps[]>
|
||||
: Promise.resolve([])
|
||||
]);
|
||||
|
||||
@@ -221,8 +222,8 @@ export default function BookList() {
|
||||
return;
|
||||
}
|
||||
const [localBooks, localSeries] = await Promise.all([
|
||||
window.electron.invoke<BookProps[]>('db:book:books'),
|
||||
window.electron.invoke<SeriesListItemProps[]>('db:series:list')
|
||||
tauri.getBooks(),
|
||||
tauri.getSeriesList() as Promise<SeriesListItemProps[]>
|
||||
]);
|
||||
booksResponse = localBooks.map(b => ({...b, itIsLocal: true}));
|
||||
seriesResponse = localSeries;
|
||||
@@ -396,25 +397,20 @@ export default function BookList() {
|
||||
let bookResponse: BookProps | null = null;
|
||||
|
||||
// DUAL LOGIC
|
||||
if (isCurrentlyOffline()) {
|
||||
if (!offlineMode.isDatabaseInitialized) {
|
||||
const isOfflineBook = localOnlyBooks.find((book: SyncedBook): boolean => book.id === bookId);
|
||||
if (isCurrentlyOffline() || isOfflineBook) {
|
||||
if (isCurrentlyOffline() && !offlineMode.isDatabaseInitialized) {
|
||||
errorMessage(t("bookList.errorBookDetails"));
|
||||
return;
|
||||
}
|
||||
bookResponse = await window.electron.invoke('db:book:bookBasicInformation', bookId);
|
||||
bookResponse = await tauri.getBookBasicInformation(bookId);
|
||||
if (bookResponse) localBookOnly = true;
|
||||
} else {
|
||||
const isOfflineBook = localOnlyBooks.find((book: SyncedBook): boolean => book.id === bookId);
|
||||
if (isOfflineBook) {
|
||||
bookResponse = await window.electron.invoke('db:book:bookBasicInformation', bookId);
|
||||
localBookOnly = true;
|
||||
}
|
||||
if (!bookResponse) {
|
||||
bookResponse = await System.authGetQueryToServer<BookProps>(
|
||||
'book/basic-information', accessToken, lang, {id: bookId}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!bookResponse) {
|
||||
errorMessage(t("bookList.errorBookDetails"));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client'
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faFeather, faTimes} from "@fortawesome/free-solid-svg-icons";
|
||||
import {ChangeEvent, forwardRef, useContext, useImperativeHandle, useState} from "react";
|
||||
@@ -131,12 +132,12 @@ function BasicInformationSetting(props: any, ref: any) {
|
||||
bookId: bookId
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:updateBasicInformation', basicInfoData);
|
||||
response = await tauri.updateBookBasicInfo(basicInfoData);
|
||||
} else {
|
||||
response = await System.authPostToServer<boolean>('book/basic-information', basicInfoData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:book:updateBasicInformation', basicInfoData);
|
||||
addToQueue('update_book_basic_info', {data: basicInfoData});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faTrash} from "@fortawesome/free-solid-svg-icons";
|
||||
import {useContext, useState} from "react";
|
||||
@@ -40,7 +41,7 @@ export default function DeleteBook({bookId}: DeleteBookProps) {
|
||||
const deleteData = { id: bookId, deletedAt };
|
||||
|
||||
if (isCurrentlyOffline() || ifLocalOnlyBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:delete', deleteData);
|
||||
response = await tauri.deleteBook(deleteData.id, deleteData.deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>(
|
||||
`book/delete`,
|
||||
@@ -50,7 +51,7 @@ export default function DeleteBook({bookId}: DeleteBookProps) {
|
||||
);
|
||||
// If synced book and user wants to delete local too
|
||||
if (response && ifSyncedBook && deleteLocalToo) {
|
||||
await window.electron.invoke<boolean>('db:book:delete', deleteData);
|
||||
await tauri.deleteBook(deleteData.id, deleteData.deletedAt);
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client'
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import React, {useCallback, useContext, useEffect, useState} from 'react';
|
||||
import {useTranslations} from 'next-intl';
|
||||
import {BookContext} from '@/context/BookContext';
|
||||
@@ -33,10 +34,7 @@ export default function ExportSetting(): React.JSX.Element {
|
||||
if (!book) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const chaptersInfo: ChapterExportInfo[] = await window.electron.invoke<ChapterExportInfo[]>(
|
||||
'db:book:export:info',
|
||||
{bookId: book.bookId}
|
||||
);
|
||||
const chaptersInfo: ChapterExportInfo[] = await tauri.getBookExportInfo(book.bookId) as ChapterExportInfo[];
|
||||
setChapters(chaptersInfo);
|
||||
const initialSelections: ChapterExportSelection[] = chaptersInfo.map(
|
||||
(ch: ChapterExportInfo): ChapterExportSelection => ({
|
||||
@@ -92,7 +90,7 @@ export default function ExportSetting(): React.JSX.Element {
|
||||
.filter((s: ChapterExportSelection): boolean => s.selected)
|
||||
.map((s: ChapterExportSelection) => ({chapterId: s.chapterId, version: s.version}));
|
||||
|
||||
const result: boolean = await window.electron.invoke<boolean>('db:book:export', {
|
||||
const result: boolean = await tauri.exportBook({
|
||||
bookId: book.bookId,
|
||||
format,
|
||||
selections: selectedChapters.length === chapters.length ? null : selectedChapters
|
||||
|
||||
@@ -14,6 +14,7 @@ import {LangContext} from '@/context/LangContext';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BookContext} from '@/context/BookContext';
|
||||
import System from '@/lib/models/System';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
type AttributeResponse = { type: string; values: Attribute[] }[];
|
||||
|
||||
@@ -49,10 +50,8 @@ export default function CharacterEditorDetail({
|
||||
async function getAttributes(): Promise<void> {
|
||||
try {
|
||||
let response: AttributeResponse;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<AttributeResponse>('db:character:attributes', {characterId: character?.id});
|
||||
} else if (book?.localBook) {
|
||||
response = await window.electron.invoke<AttributeResponse>('db:character:attributes', {characterId: character?.id});
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await tauri.getCharacterAttributes(character?.id!) as AttributeResponse;
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<AttributeResponse>(
|
||||
'character/attribute',
|
||||
|
||||
@@ -28,6 +28,7 @@ import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BookContext} from '@/context/BookContext';
|
||||
import System from '@/lib/models/System';
|
||||
import {Dispatch, SetStateAction} from 'react';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
type AttributeResponse = { type: string; values: Attribute[] }[];
|
||||
|
||||
@@ -84,10 +85,8 @@ export default function CharacterEditorEdit({
|
||||
async function getAttributes(): Promise<void> {
|
||||
try {
|
||||
let response: AttributeResponse;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<AttributeResponse>('db:character:attributes', {characterId: character?.id});
|
||||
} else if (book?.localBook) {
|
||||
response = await window.electron.invoke<AttributeResponse>('db:character:attributes', {characterId: character?.id});
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await tauri.getCharacterAttributes(character?.id!) as AttributeResponse;
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<AttributeResponse>(
|
||||
'character/attribute',
|
||||
|
||||
@@ -39,6 +39,7 @@ import {LangContext} from '@/context/LangContext';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BookContext} from '@/context/BookContext';
|
||||
import System from '@/lib/models/System';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
type AttributeResponse = { type: string; values: Attribute[] }[];
|
||||
|
||||
@@ -70,10 +71,8 @@ export default function CharacterSettingsDetail({
|
||||
async function getAttributes(): Promise<void> {
|
||||
try {
|
||||
let response: AttributeResponse;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<AttributeResponse>('db:character:attributes', {characterId: character?.id});
|
||||
} else if (book?.localBook) {
|
||||
response = await window.electron.invoke<AttributeResponse>('db:character:attributes', {characterId: character?.id});
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await tauri.getCharacterAttributes(character?.id!) as AttributeResponse;
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<AttributeResponse>(
|
||||
'character/attribute',
|
||||
|
||||
@@ -37,6 +37,7 @@ import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {BookContext} from '@/context/BookContext';
|
||||
import System from '@/lib/models/System';
|
||||
import {Dispatch, SetStateAction} from 'react';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
type AttributeResponse = { type: string; values: Attribute[] }[];
|
||||
|
||||
@@ -94,10 +95,8 @@ export default function CharacterSettingsEdit({
|
||||
async function getAttributes(): Promise<void> {
|
||||
try {
|
||||
let response: AttributeResponse;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<AttributeResponse>('db:character:attributes', {characterId: character?.id});
|
||||
} else if (book?.localBook) {
|
||||
response = await window.electron.invoke<AttributeResponse>('db:character:attributes', {characterId: character?.id});
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await tauri.getCharacterAttributes(character?.id!) as AttributeResponse;
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<AttributeResponse>(
|
||||
'character/attribute',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client'
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import {ChangeEvent, forwardRef, useContext, useEffect, useImperativeHandle, useState} from 'react';
|
||||
import System from '@/lib/models/System';
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
@@ -83,15 +84,11 @@ function GuideLineSetting(props: any, ref: any) {
|
||||
async function getAIGuideLine(): Promise<void> {
|
||||
try {
|
||||
let response: GuideLineAI;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<GuideLineAI>('db:book:guideline:ai:get', {id: bookId});
|
||||
} else {
|
||||
if (book?.localBook) {
|
||||
response = await window.electron.invoke<GuideLineAI>('db:book:guideline:ai:get', {id: bookId});
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await tauri.getAIGuideLine(bookId);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<GuideLineAI>(`book/ai/guideline`, userToken, lang, {id: bookId});
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
setPlotSummary(response.globalResume || '');
|
||||
setVerbTense(response.verbeTense?.toString() || '');
|
||||
@@ -113,11 +110,8 @@ function GuideLineSetting(props: any, ref: any) {
|
||||
async function getGuideLine(): Promise<void> {
|
||||
try {
|
||||
let response: GuideLine;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<GuideLine>('db:book:guideline:get', {id: bookId});
|
||||
} else {
|
||||
if (book?.localBook) {
|
||||
response = await window.electron.invoke<GuideLine>('db:book:guideline:get', {id: bookId});
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await tauri.getGuideLine(bookId);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<GuideLine>(
|
||||
`book/guide-line`,
|
||||
@@ -126,7 +120,6 @@ function GuideLineSetting(props: any, ref: any) {
|
||||
{id: bookId},
|
||||
);
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
setTone(response.tone);
|
||||
setAtmosphere(response.atmosphere);
|
||||
@@ -165,7 +158,7 @@ function GuideLineSetting(props: any, ref: any) {
|
||||
keyMessages: keyMessages,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:guideline:update', guidelineData);
|
||||
response = await tauri.updateGuideLine(guidelineData);
|
||||
} else {
|
||||
response = await System.authPostToServer<boolean>(
|
||||
'book/guide-line',
|
||||
@@ -175,7 +168,7 @@ function GuideLineSetting(props: any, ref: any) {
|
||||
);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:book:guideline:update', guidelineData);
|
||||
addToQueue('update_guideline', {data: guidelineData});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
@@ -206,7 +199,7 @@ function GuideLineSetting(props: any, ref: any) {
|
||||
themes: themes,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:guideline:ai:update', aiGuidelineData);
|
||||
response = await tauri.updateAIGuideLine(aiGuidelineData);
|
||||
} else {
|
||||
response = await System.authPostToServer<boolean>(
|
||||
'quillsense/book/guide-line',
|
||||
@@ -216,7 +209,7 @@ function GuideLineSetting(props: any, ref: any) {
|
||||
);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:book:guideline:ai:update', aiGuidelineData);
|
||||
addToQueue('update_ai_guideline', {data: aiGuidelineData});
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import {SyncedSeries} from "@/lib/models/SyncedSeries";
|
||||
import ToggleSwitch from "@/components/form/ToggleSwitch";
|
||||
import {SeriesLocationElement, SeriesLocationItem, SeriesLocationSubElement} from "@/lib/models/Series";
|
||||
import SeriesImportSelector from "@/components/form/SeriesImportSelector";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface SubElement {
|
||||
id: string;
|
||||
@@ -120,11 +121,7 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
try {
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:tool:update', {
|
||||
bookId: currentEntityId,
|
||||
toolName: 'locations',
|
||||
enabled: enabled
|
||||
});
|
||||
response = await tauri.updateBookToolSetting(currentEntityId, 'locations', enabled);
|
||||
} else {
|
||||
response = await System.authPatchToServer<boolean>('book/tool-setting', {
|
||||
bookId: currentEntityId,
|
||||
@@ -132,11 +129,11 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
enabled: enabled
|
||||
}, token, lang);
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
|
||||
addToQueue('db:book:tool:update', {
|
||||
addToQueue('update_book_tool_setting', {data: {
|
||||
bookId: currentEntityId,
|
||||
toolName: 'locations',
|
||||
enabled: enabled
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (response && setBook && book) {
|
||||
@@ -162,7 +159,7 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
if (isSeriesMode) {
|
||||
let response: SeriesLocationItem[];
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<SeriesLocationItem[]>('db:series:location:list', {seriesId: currentEntityId});
|
||||
response = await tauri.getSeriesLocationList(currentEntityId) as SeriesLocationItem[];
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesLocationItem[]>(
|
||||
'series/location/list',
|
||||
@@ -190,17 +187,13 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
}
|
||||
} else {
|
||||
let response: LocationListResponse;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<LocationListResponse>('db:location:all', {bookid: currentEntityId});
|
||||
} else {
|
||||
if (book?.localBook) {
|
||||
response = await window.electron.invoke<LocationListResponse>('db:location:all', {bookid: currentEntityId});
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await tauri.getAllLocations(currentEntityId, true) as LocationListResponse;
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<LocationListResponse>(`location/all`, token, lang, {
|
||||
bookid: currentEntityId,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
setSections(response.locations);
|
||||
setToolEnabled(response.enabled);
|
||||
@@ -238,7 +231,7 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
name: newSectionName,
|
||||
};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
sectionId = await window.electron.invoke<string>('db:series:location:section:add', addData);
|
||||
sectionId = await tauri.addSeriesLocationSection(addData);
|
||||
} else {
|
||||
sectionId = await System.authPostToServer<string>(
|
||||
'series/location/section/add',
|
||||
@@ -247,7 +240,7 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
lang
|
||||
);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
|
||||
addToQueue('db:series:location:section:add', addData);
|
||||
addToQueue('add_series_location_section', {data: addData});
|
||||
}
|
||||
}
|
||||
if (!sectionId) {
|
||||
@@ -255,10 +248,7 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
return;
|
||||
}
|
||||
} else if (isCurrentlyOffline() || book?.localBook) {
|
||||
sectionId = await window.electron.invoke<string>('db:location:section:add', {
|
||||
bookId: currentEntityId,
|
||||
locationName: newSectionName,
|
||||
});
|
||||
sectionId = await tauri.addLocationSection(newSectionName, currentEntityId);
|
||||
} else {
|
||||
sectionId = await System.authPostToServer<string>(`location/section/add`, {
|
||||
bookId: currentEntityId,
|
||||
@@ -266,11 +256,11 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
}, token, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
|
||||
addToQueue('db:location:section:add', {
|
||||
addToQueue('add_location_section', {data: {
|
||||
bookId: currentEntityId,
|
||||
sectionId,
|
||||
locationName: newSectionName,
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (!sectionId) {
|
||||
@@ -306,7 +296,7 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
name: newElementNames[sectionId],
|
||||
};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
elementId = await window.electron.invoke<string>('db:series:location:element:add', addData);
|
||||
elementId = await tauri.addSeriesLocationElement(addData);
|
||||
} else {
|
||||
elementId = await System.authPostToServer<string>(
|
||||
'series/location/element/add',
|
||||
@@ -315,7 +305,7 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
lang
|
||||
);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
|
||||
addToQueue('db:series:location:element:add', addData);
|
||||
addToQueue('add_series_location_element', {data: addData});
|
||||
}
|
||||
}
|
||||
if (!elementId) {
|
||||
@@ -323,11 +313,7 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
return;
|
||||
}
|
||||
} else if (isCurrentlyOffline() || book?.localBook) {
|
||||
elementId = await window.electron.invoke<string>('db:location:element:add', {
|
||||
bookId: currentEntityId,
|
||||
locationId: sectionId,
|
||||
elementName: newElementNames[sectionId],
|
||||
});
|
||||
elementId = await tauri.addLocationElement(sectionId, newElementNames[sectionId]);
|
||||
} else {
|
||||
elementId = await System.authPostToServer<string>(`location/element/add`, {
|
||||
bookId: currentEntityId,
|
||||
@@ -337,12 +323,12 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
token, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
|
||||
addToQueue('db:location:element:add', {
|
||||
addToQueue('add_location_element', {data: {
|
||||
bookId: currentEntityId,
|
||||
locationId: sectionId,
|
||||
elementId,
|
||||
elementName: newElementNames[sectionId],
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (!elementId) {
|
||||
@@ -405,7 +391,7 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
name: newSubElementNames[elementIndex],
|
||||
};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
subElementId = await window.electron.invoke<string>('db:series:location:subelement:add', addData);
|
||||
subElementId = await tauri.addSeriesLocationSubElement(addData);
|
||||
} else {
|
||||
subElementId = await System.authPostToServer<string>(
|
||||
'series/location/sub-element/add',
|
||||
@@ -414,7 +400,7 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
lang
|
||||
);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
|
||||
addToQueue('db:series:location:subelement:add', addData);
|
||||
addToQueue('add_series_location_sub_element', {data: addData});
|
||||
}
|
||||
}
|
||||
if (!subElementId) {
|
||||
@@ -422,10 +408,7 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
return;
|
||||
}
|
||||
} else if (isCurrentlyOffline() || book?.localBook) {
|
||||
subElementId = await window.electron.invoke<string>('db:location:subelement:add', {
|
||||
elementId: elementId,
|
||||
subElementName: newSubElementNames[elementIndex],
|
||||
});
|
||||
subElementId = await tauri.addLocationSubElement(elementId, newSubElementNames[elementIndex]);
|
||||
} else {
|
||||
subElementId = await System.authPostToServer<string>(`location/sub-element/add`, {
|
||||
elementId: elementId,
|
||||
@@ -433,11 +416,11 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
}, token, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
|
||||
addToQueue('db:location:subelement:add', {
|
||||
addToQueue('add_location_sub_element', {data: {
|
||||
elementId: elementId,
|
||||
subElementId,
|
||||
subElementName: newSubElementNames[elementIndex],
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (!subElementId) {
|
||||
@@ -490,26 +473,24 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
if (isSeriesMode) {
|
||||
const deleteData = {elementId: elementId, deletedAt};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<boolean>('db:series:location:element:delete', deleteData);
|
||||
response = await tauri.deleteSeriesLocationElement(deleteData.elementId!, deleteData.deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>('series/location/element/delete', deleteData, token, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
|
||||
addToQueue('db:series:location:element:delete', deleteData);
|
||||
addToQueue('delete_series_location_element', {data: deleteData});
|
||||
}
|
||||
}
|
||||
} else if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:location:element:delete', {
|
||||
elementId: elementId, bookId: currentEntityId, deletedAt,
|
||||
});
|
||||
response = await tauri.deleteLocationElement(elementId!, currentEntityId, deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>(`location/element/delete`, {
|
||||
elementId: elementId, bookId: currentEntityId, deletedAt,
|
||||
}, token, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
|
||||
addToQueue('db:location:element:delete', {
|
||||
addToQueue('delete_location_element', {data: {
|
||||
elementId: elementId, bookId: currentEntityId, deletedAt,
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
@@ -541,26 +522,24 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
if (isSeriesMode) {
|
||||
const deleteData = {subElementId: subElementId, deletedAt};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<boolean>('db:series:location:subelement:delete', deleteData);
|
||||
response = await tauri.deleteSeriesLocationSubElement(deleteData.subElementId!, deleteData.deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>('series/location/sub-element/delete', deleteData, token, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
|
||||
addToQueue('db:series:location:subelement:delete', deleteData);
|
||||
addToQueue('delete_series_location_sub_element', {data: deleteData});
|
||||
}
|
||||
}
|
||||
} else if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:location:subelement:delete', {
|
||||
subElementId: subElementId, bookId: currentEntityId, deletedAt,
|
||||
});
|
||||
response = await tauri.deleteLocationSubElement(subElementId!, currentEntityId, deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>(`location/sub-element/delete`, {
|
||||
subElementId: subElementId, bookId: currentEntityId, deletedAt,
|
||||
}, token, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
|
||||
addToQueue('db:location:subelement:delete', {
|
||||
addToQueue('delete_location_sub_element', {data: {
|
||||
subElementId: subElementId, bookId: currentEntityId, deletedAt,
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
@@ -587,26 +566,24 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
if (isSeriesMode) {
|
||||
const deleteData = {locationId: sectionId, deletedAt};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<boolean>('db:series:location:delete', deleteData);
|
||||
response = await tauri.deleteSeriesLocation(deleteData.locationId, deleteData.deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>('series/location/delete', deleteData, token, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
|
||||
addToQueue('db:series:location:delete', deleteData);
|
||||
addToQueue('delete_series_location', {data: deleteData});
|
||||
}
|
||||
}
|
||||
} else if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:location:delete', {
|
||||
locationId: sectionId, bookId: currentEntityId, deletedAt,
|
||||
});
|
||||
response = await tauri.deleteLocationSection(sectionId, currentEntityId, deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>(`location/delete`, {
|
||||
locationId: sectionId, bookId: currentEntityId, deletedAt,
|
||||
}, token, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
|
||||
addToQueue('db:location:delete', {
|
||||
addToQueue('delete_location_section', {data: {
|
||||
locationId: sectionId, bookId: currentEntityId, deletedAt,
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
@@ -628,18 +605,16 @@ export function LocationComponent(props: LocationComponentProps, ref: React.Ref<
|
||||
try {
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:location:update', {
|
||||
locations: sections,
|
||||
});
|
||||
response = await tauri.updateLocations(sections) as boolean;
|
||||
} else {
|
||||
response = await System.authPostToServer<boolean>(`location/update`, {
|
||||
locations: sections,
|
||||
}, token, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
|
||||
addToQueue('db:location:update', {
|
||||
addToQueue('update_locations', {data: {
|
||||
locations: sections,
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
|
||||
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
|
||||
import {SyncedBook} from "@/lib/models/SyncedBook";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface ActProps {
|
||||
acts: ActType[];
|
||||
@@ -80,10 +81,7 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
|
||||
try {
|
||||
let incidentId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
incidentId = await window.electron.invoke<string>('db:book:incident:add', {
|
||||
bookId,
|
||||
name: newIncidentTitle,
|
||||
});
|
||||
incidentId = await tauri.addIncident(bookId!, newIncidentTitle);
|
||||
} else {
|
||||
incidentId = await System.authPostToServer<string>('book/incident/new', {
|
||||
bookId,
|
||||
@@ -91,11 +89,11 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
|
||||
}, token, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:book:incident:add', {
|
||||
addToQueue('add_incident', {data: {
|
||||
bookId,
|
||||
incidentId,
|
||||
name: newIncidentTitle,
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (!incidentId) {
|
||||
@@ -134,12 +132,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
|
||||
let response: boolean;
|
||||
const deleteData = { bookId, incidentId, deletedAt: System.timeStampInSeconds() };
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:incident:remove', deleteData);
|
||||
response = await tauri.removeIncident(deleteData.bookId!, deleteData.incidentId, deleteData.deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>('book/incident/remove', deleteData, token, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:book:incident:remove', deleteData);
|
||||
addToQueue('remove_incident', {data: deleteData});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
@@ -177,15 +175,15 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
|
||||
incidentId: selectedIncidentId,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
plotId = await window.electron.invoke<string>('db:book:plot:add', plotData);
|
||||
plotId = await tauri.addPlotPoint(plotData.bookId!, plotData.name, plotData.incidentId);
|
||||
} else {
|
||||
plotId = await System.authPostToServer<string>('book/plot/new', plotData, token, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:book:plot:add', {
|
||||
addToQueue('add_plot_point', {data: {
|
||||
...plotData,
|
||||
plotId,
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (!plotId) {
|
||||
@@ -225,12 +223,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
|
||||
let response: boolean;
|
||||
const deleteData = { plotId: plotPointId, bookId, deletedAt: System.timeStampInSeconds() };
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:plot:remove', deleteData);
|
||||
response = await tauri.removePlotPoint(deleteData.plotId, deleteData.bookId!, deleteData.deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>('book/plot/remove', deleteData, token, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:book:plot:remove', deleteData);
|
||||
addToQueue('remove_plot_point', {data: deleteData});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
@@ -279,15 +277,15 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
|
||||
incidentId: destination === 'incident' ? itemId : null,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
linkId = await window.electron.invoke<string>('db:chapter:information:add', linkData);
|
||||
linkId = await tauri.addChapterInformation(linkData as any);
|
||||
} else {
|
||||
linkId = await System.authPostToServer<string>('chapter/resume/add', linkData, token, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:chapter:information:add', {
|
||||
addToQueue('add_chapter_information', {data: {
|
||||
...linkData,
|
||||
chapterInfoId: linkId,
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (!linkId) {
|
||||
@@ -367,12 +365,12 @@ export default function Act({acts, setActs, mainChapters}: ActProps) {
|
||||
let response: boolean;
|
||||
const unlinkData = { chapterInfoId, bookId, deletedAt: System.timeStampInSeconds() };
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:chapter:information:remove', unlinkData);
|
||||
response = await tauri.removeChapterInformation(unlinkData.chapterInfoId, unlinkData.bookId!, unlinkData.deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>('chapter/resume/remove', unlinkData, token, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:chapter:information:remove', unlinkData);
|
||||
addToQueue('remove_chapter_information', {data: unlinkData});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
|
||||
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
|
||||
import {SyncedBook} from "@/lib/models/SyncedBook";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface IssuesProps {
|
||||
issues: Issue[];
|
||||
@@ -42,10 +43,7 @@ export default function Issues({issues, setIssues}: IssuesProps) {
|
||||
try {
|
||||
let issueId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
issueId = await window.electron.invoke<string>('db:book:issue:add', {
|
||||
bookId,
|
||||
name: newIssueName,
|
||||
});
|
||||
issueId = await tauri.addIssue(bookId!, newIssueName);
|
||||
} else {
|
||||
issueId = await System.authPostToServer<string>('book/issue/add', {
|
||||
bookId,
|
||||
@@ -53,11 +51,11 @@ export default function Issues({issues, setIssues}: IssuesProps) {
|
||||
}, token, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:book:issue:add', {
|
||||
addToQueue('add_issue', {data: {
|
||||
bookId,
|
||||
issueId,
|
||||
name: newIssueName,
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (!issueId) {
|
||||
@@ -90,11 +88,7 @@ export default function Issues({issues, setIssues}: IssuesProps) {
|
||||
let response: boolean;
|
||||
const deletedAt: number = System.timeStampInSeconds();
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:issue:remove', {
|
||||
bookId,
|
||||
issueId,
|
||||
deletedAt,
|
||||
});
|
||||
response = await tauri.removeIssue(bookId!, issueId, deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>(
|
||||
'book/issue/remove',
|
||||
@@ -108,11 +102,11 @@ export default function Issues({issues, setIssues}: IssuesProps) {
|
||||
);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:book:issue:remove', {
|
||||
addToQueue('remove_issue', {data: {
|
||||
bookId,
|
||||
issueId,
|
||||
deletedAt,
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
|
||||
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
|
||||
import {SyncedBook} from "@/lib/models/SyncedBook";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface MainChapterProps {
|
||||
chapters: ChapterListProps[];
|
||||
@@ -93,12 +94,12 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) {
|
||||
deletedAt,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:chapter:remove', deleteData);
|
||||
response = await tauri.removeChapter(deleteData.chapterId, deleteData.bookId!, deleteData.deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>('chapter/remove', deleteData, token, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:chapter:remove', deleteData);
|
||||
addToQueue('remove_chapter', {data: deleteData});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
@@ -129,15 +130,15 @@ export default function MainChapter({chapters, setChapters}: MainChapterProps) {
|
||||
title: newChapterTitle,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
responseId = await window.electron.invoke<string>('db:chapter:add', chapterData);
|
||||
responseId = await tauri.addChapter(chapterData);
|
||||
} else {
|
||||
responseId = await System.authPostToServer<string>('chapter/add', chapterData, token);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:chapter:add', {
|
||||
addToQueue('add_chapter', {data: {
|
||||
...chapterData,
|
||||
chapterId: responseId,
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (!responseId) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
|
||||
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
|
||||
import {SyncedBook} from "@/lib/models/SyncedBook";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
export const StoryContext = createContext<{
|
||||
acts: ActType[];
|
||||
@@ -76,17 +77,13 @@ export function Story(props: any, ref: any) {
|
||||
async function getStoryData(): Promise<void> {
|
||||
try {
|
||||
let response: StoryFetchData;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<StoryFetchData>('db:book:story:get', {bookid: bookId});
|
||||
} else {
|
||||
if (book?.localBook) {
|
||||
response = await window.electron.invoke<StoryFetchData>('db:book:story:get', {bookid: bookId});
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await tauri.getBookStory(bookId) as StoryFetchData;
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<StoryFetchData>(`book/story`, userToken, lang, {
|
||||
bookid: bookId,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
setActs(response.acts);
|
||||
setMainChapters(response.mainChapter);
|
||||
@@ -143,12 +140,12 @@ export function Story(props: any, ref: any) {
|
||||
issues,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:story:update', storyData);
|
||||
response = await tauri.updateBookStory(storyData);
|
||||
} else {
|
||||
response = await System.authPostToServer<boolean>('book/story', storyData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:book:story:update', storyData);
|
||||
addToQueue('update_book_story', {data: storyData});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {SyncedBook} from "@/lib/models/SyncedBook";
|
||||
import {SeriesContext, SeriesContextProps} from "@/context/SeriesContext";
|
||||
import {SeriesSyncContext, SeriesSyncContextProps} from "@/context/SeriesSyncContext";
|
||||
import {SyncedSeries} from "@/lib/models/SyncedSeries";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface WorldElementInputProps {
|
||||
sectionLabel: string;
|
||||
@@ -69,26 +70,24 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
|
||||
if (isSeriesMode) {
|
||||
const deleteData = {elementId, deletedAt};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<boolean>('db:series:world:element:delete', deleteData);
|
||||
response = await tauri.deleteSeriesWorldElement(elementId, deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>('series/world/element/delete', deleteData, session.accessToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
|
||||
addToQueue('db:series:world:element:delete', deleteData);
|
||||
addToQueue('delete_series_world_element', {data: deleteData});
|
||||
}
|
||||
}
|
||||
} else if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:world:element:remove', {
|
||||
elementId, bookId: book?.bookId, deletedAt,
|
||||
});
|
||||
response = await tauri.removeWorldElement(elementId, book?.bookId || '', deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>('book/world/element/delete', {
|
||||
elementId, bookId: book?.bookId, deletedAt,
|
||||
}, session.accessToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
|
||||
addToQueue('db:book:world:element:remove', {
|
||||
addToQueue('remove_world_element', {data: {
|
||||
elementId: elementId, bookId: book?.bookId, deletedAt,
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
@@ -123,7 +122,7 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
|
||||
name: newElementName,
|
||||
};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
elementId = await window.electron.invoke<string>('db:series:world:element:add', addData);
|
||||
elementId = await tauri.addSeriesWorldElement(addData);
|
||||
} else {
|
||||
elementId = await System.authPostToServer<string>(
|
||||
'series/world/element/add',
|
||||
@@ -132,7 +131,7 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
|
||||
lang
|
||||
);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
|
||||
addToQueue('db:series:world:element:add', addData);
|
||||
addToQueue('add_series_world_element', {data: addData});
|
||||
}
|
||||
}
|
||||
if (!elementId) {
|
||||
@@ -140,11 +139,7 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
|
||||
return;
|
||||
}
|
||||
} else if (isCurrentlyOffline() || book?.localBook) {
|
||||
elementId = await window.electron.invoke<string>('db:book:world:element:add', {
|
||||
elementType: section,
|
||||
worldId: worlds[selectedWorldIndex].id,
|
||||
elementName: newElementName,
|
||||
});
|
||||
elementId = await tauri.addWorldElement(worlds[selectedWorldIndex].id, newElementName, section as string);
|
||||
} else {
|
||||
elementId = await System.authPostToServer('book/world/element/add', {
|
||||
elementType: section,
|
||||
@@ -153,12 +148,12 @@ export default function WorldElementComponent({sectionLabel, sectionType}: World
|
||||
}, session.accessToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
|
||||
addToQueue('db:book:world:element:add', {
|
||||
addToQueue('add_world_element', {data: {
|
||||
elementType: section,
|
||||
worldId: worlds[selectedWorldIndex].id,
|
||||
elementId,
|
||||
elementName: newElementName,
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (!elementId) {
|
||||
|
||||
@@ -24,6 +24,7 @@ import ToggleSwitch from "@/components/form/ToggleSwitch";
|
||||
import {SeriesWorldProps, SeriesWorldListItem} from "@/lib/models/Series";
|
||||
import SeriesImportSelector from "@/components/form/SeriesImportSelector";
|
||||
import SyncFieldWrapper from "@/components/form/SyncFieldWrapper";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
export interface ElementSection {
|
||||
title: string;
|
||||
@@ -99,11 +100,7 @@ export function WorldSetting(props: WorldSettingProps, ref: React.Ref<{ handleSa
|
||||
try {
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:tool:update', {
|
||||
bookId: currentEntityId,
|
||||
toolName: 'worlds',
|
||||
enabled: enabled
|
||||
});
|
||||
response = await tauri.updateBookToolSetting(currentEntityId, 'worlds', enabled);
|
||||
} else {
|
||||
response = await System.authPatchToServer<boolean>('book/tool-setting', {
|
||||
bookId: currentEntityId,
|
||||
@@ -111,11 +108,11 @@ export function WorldSetting(props: WorldSettingProps, ref: React.Ref<{ handleSa
|
||||
enabled: enabled
|
||||
}, session.accessToken, lang);
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
|
||||
addToQueue('db:book:tool:update', {
|
||||
addToQueue('update_book_tool_setting', {data: {
|
||||
bookId: currentEntityId,
|
||||
toolName: 'worlds',
|
||||
enabled: enabled
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
if (response && setBook && book) {
|
||||
@@ -176,7 +173,7 @@ export function WorldSetting(props: WorldSettingProps, ref: React.Ref<{ handleSa
|
||||
// Book mode: dual offline/online logic
|
||||
let response: WorldListResponse;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<WorldListResponse>('db:book:worlds:get', {bookid: currentEntityId});
|
||||
response = await tauri.getWorlds(currentEntityId, true);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<WorldListResponse>('book/worlds', session.accessToken, lang, {
|
||||
bookid: currentEntityId,
|
||||
@@ -237,10 +234,7 @@ export function WorldSetting(props: WorldSettingProps, ref: React.Ref<{ handleSa
|
||||
}
|
||||
} else if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Book mode: offline/local
|
||||
newWorldId = await window.electron.invoke<string>('db:book:world:add', {
|
||||
worldName: newWorldName,
|
||||
bookId: currentEntityId,
|
||||
});
|
||||
newWorldId = await tauri.addWorld(currentEntityId, newWorldName);
|
||||
if (!newWorldId) {
|
||||
errorMessage(t("worldSetting.addWorldError"));
|
||||
return;
|
||||
@@ -256,11 +250,11 @@ export function WorldSetting(props: WorldSettingProps, ref: React.Ref<{ handleSa
|
||||
return;
|
||||
}
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
|
||||
addToQueue('db:book:world:add', {
|
||||
addToQueue('add_world', {data: {
|
||||
worldName: newWorldName,
|
||||
worldId: newWorldId,
|
||||
bookId: currentEntityId,
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
const newWorld: WorldProps = {
|
||||
@@ -319,10 +313,7 @@ export function WorldSetting(props: WorldSettingProps, ref: React.Ref<{ handleSa
|
||||
}, session.accessToken, lang);
|
||||
} else if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Book mode: offline/local
|
||||
response = await window.electron.invoke<boolean>('db:book:world:update', {
|
||||
world: currentWorld,
|
||||
bookId: currentEntityId,
|
||||
});
|
||||
response = await tauri.updateWorld(currentWorld);
|
||||
} else {
|
||||
// Book mode: online
|
||||
response = await System.authPatchToServer<boolean>('book/world/update', {
|
||||
@@ -330,10 +321,10 @@ export function WorldSetting(props: WorldSettingProps, ref: React.Ref<{ handleSa
|
||||
bookId: currentEntityId,
|
||||
}, session.accessToken, lang);
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
|
||||
addToQueue('db:book:world:update', {
|
||||
addToQueue('update_world', {data: {
|
||||
world: currentWorld,
|
||||
bookId: currentEntityId,
|
||||
});
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,11 +415,11 @@ export function WorldSetting(props: WorldSettingProps, ref: React.Ref<{ handleSa
|
||||
|
||||
// Sync to local if book is synced
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === currentEntityId)) {
|
||||
addToQueue('db:book:world:add', {
|
||||
addToQueue('add_world', {data: {
|
||||
worldName: seriesWorld.name,
|
||||
worldId: worldId,
|
||||
bookId: currentEntityId,
|
||||
});
|
||||
}});
|
||||
}
|
||||
|
||||
const newWorld: WorldProps = {
|
||||
|
||||
@@ -34,6 +34,7 @@ import {AIUsageContext, AIUsageContextProps} from "@/context/AIUsageContext";
|
||||
import {configs} from "@/lib/configs";
|
||||
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import AdvancedGenerationOptions from "@/components/form/AdvancedGenerationOptions";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface CompanionContent {
|
||||
version: number;
|
||||
@@ -113,19 +114,11 @@ export default function DraftCompanion() {
|
||||
async function getDraftContent(): Promise<void> {
|
||||
try {
|
||||
let response: CompanionContent | null;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<CompanionContent>('db:chapter:content:companion', {
|
||||
bookid: book?.bookId,
|
||||
chapterid: chapter?.chapterId,
|
||||
version: chapter?.chapterContent.version,
|
||||
});
|
||||
} else {
|
||||
if (book?.localBook) {
|
||||
response = await window.electron.invoke<CompanionContent>('db:chapter:content:companion', {
|
||||
bookid: book?.bookId,
|
||||
chapterid: chapter?.chapterId,
|
||||
version: chapter?.chapterContent.version,
|
||||
});
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await tauri.getCompanionContent(
|
||||
chapter?.chapterId ?? '',
|
||||
chapter?.chapterContent.version ?? 0,
|
||||
) as CompanionContent | null;
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<CompanionContent>(`chapter/content/companion`, session.accessToken, lang, {
|
||||
bookid: book?.bookId,
|
||||
@@ -133,7 +126,6 @@ export default function DraftCompanion() {
|
||||
version: chapter?.chapterContent.version,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (response && mainEditor) {
|
||||
mainEditor.commands.setContent(JSON.parse(response.content));
|
||||
setDraftVersion(response.version);
|
||||
@@ -169,17 +161,13 @@ export default function DraftCompanion() {
|
||||
async function fetchTags(): Promise<void> {
|
||||
try {
|
||||
let responseTags: BookTags | null;
|
||||
if (isCurrentlyOffline()) {
|
||||
responseTags = await window.electron.invoke<BookTags>('db:book:tags', book?.bookId);
|
||||
} else {
|
||||
if (book?.localBook) {
|
||||
responseTags = await window.electron.invoke<BookTags>('db:book:tags', book?.bookId);
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
responseTags = await tauri.getBookTags(book?.bookId ?? '') as BookTags | null;
|
||||
} else {
|
||||
responseTags = await System.authGetQueryToServer<BookTags>(`book/tags`, session.accessToken, lang, {
|
||||
bookId: book?.bookId
|
||||
});
|
||||
}
|
||||
}
|
||||
if (responseTags) {
|
||||
setCharacters(responseTags.characters);
|
||||
setLocations(responseTags.locations);
|
||||
|
||||
@@ -36,6 +36,7 @@ import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
|
||||
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
|
||||
import {SyncedBook} from "@/lib/models/SyncedBook";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface ToolbarButton {
|
||||
action: () => void;
|
||||
@@ -302,12 +303,18 @@ export default function TextEditor() {
|
||||
currentTime: mainTimer
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook){
|
||||
response = await window.electron.invoke<boolean>('db:chapter:content:save', saveData);
|
||||
response = await tauri.saveChapterContent({
|
||||
chapterId: saveData.chapterId,
|
||||
version: saveData.version,
|
||||
content: saveData.content,
|
||||
totalWordCount: saveData.totalWordCount,
|
||||
contentId: saveData.chapterId,
|
||||
});
|
||||
} else {
|
||||
response = await System.authPostToServer<boolean>(`chapter/content`, saveData, session?.accessToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
|
||||
addToQueue('db:chapter:content:save', saveData);
|
||||
addToQueue('save_chapter_content', {data: saveData});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContex
|
||||
import {BookContext} from '@/context/BookContext';
|
||||
import {SyncedBook} from '@/lib/models/SyncedBook';
|
||||
import System from '@/lib/models/System';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
export type SyncElementType = 'character' | 'world' | 'location' | 'spell';
|
||||
|
||||
@@ -72,10 +73,8 @@ export default function SyncFieldWrapper({
|
||||
let response: SeriesSyncUploadResponse;
|
||||
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Offline OU livre local → IPC
|
||||
response = await window.electron.invoke<SeriesSyncUploadResponse>('db:series:sync:upload', requestData);
|
||||
response = await tauri.seriesSyncUpload(requestData) as SeriesSyncUploadResponse;
|
||||
} else {
|
||||
// Online + livre serveur → Server
|
||||
response = await System.authPostToServer<SeriesSyncUploadResponse>(
|
||||
'series/propagate',
|
||||
requestData,
|
||||
@@ -83,9 +82,8 @@ export default function SyncFieldWrapper({
|
||||
lang
|
||||
);
|
||||
|
||||
// Si le livre a une copie locale → addToQueue pour sync
|
||||
if (book?.bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book.bookId)) {
|
||||
addToQueue('db:series:sync:upload', requestData);
|
||||
addToQueue('series_sync_upload', {data: requestData});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
|
||||
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
|
||||
import {SyncedBook} from "@/lib/models/SyncedBook";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
export default function ScribeChapterComponent() {
|
||||
const t = useTranslations();
|
||||
@@ -79,15 +80,11 @@ export default function ScribeChapterComponent() {
|
||||
async function getChapterList(): Promise<void> {
|
||||
try {
|
||||
let response: ChapterListProps[]|null;
|
||||
if (isCurrentlyOffline()){
|
||||
response = await window.electron.invoke<ChapterListProps[]>('db:book:chapters', book?.bookId)
|
||||
} else {
|
||||
if (book?.localBook){
|
||||
response = await window.electron.invoke<ChapterListProps[]>('db:book:chapters', book?.bookId)
|
||||
if (isCurrentlyOffline() || book?.localBook){
|
||||
response = await tauri.getChapters(book?.bookId ?? '') as ChapterListProps[];
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<ChapterListProps[]>(`book/chapters?id=${book?.bookId}`, userToken, lang);
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
setChapters(response);
|
||||
}
|
||||
@@ -104,19 +101,8 @@ export default function ScribeChapterComponent() {
|
||||
const version: number = chapter?.chapterContent.version ? chapter?.chapterContent.version : 2;
|
||||
try {
|
||||
let response: ChapterProps | null
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<ChapterProps>('db:chapter:whole', {
|
||||
bookid: book?.bookId,
|
||||
id: chapterId,
|
||||
version: version,
|
||||
})
|
||||
} else {
|
||||
if (book?.localBook){
|
||||
response = await window.electron.invoke<ChapterProps>('db:chapter:whole', {
|
||||
bookid: book?.bookId,
|
||||
id: chapterId,
|
||||
version: version,
|
||||
})
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await tauri.getWholeChapter(chapterId, version, book?.bookId ?? '');
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<ChapterProps>(`chapter/whole`, userToken, lang, {
|
||||
bookid: book?.bookId,
|
||||
@@ -124,7 +110,6 @@ export default function ScribeChapterComponent() {
|
||||
version: version,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t("scribeChapterComponent.errorFetchChapter"));
|
||||
return;
|
||||
@@ -148,12 +133,12 @@ export default function ScribeChapterComponent() {
|
||||
title: title,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:chapter:update', updateData);
|
||||
response = await tauri.updateChapter(updateData.chapterId, updateData.title, updateData.chapterOrder);
|
||||
} else {
|
||||
response = await System.authPostToServer<boolean>('chapter/update', updateData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
|
||||
addToQueue('db:chapter:update', updateData);
|
||||
addToQueue('update_chapter', {data: updateData});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
@@ -190,11 +175,7 @@ export default function ScribeChapterComponent() {
|
||||
let response:boolean = false;
|
||||
const deletedAt: number = System.timeStampInSeconds();
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:chapter:remove', {
|
||||
chapterId: removeChapterId,
|
||||
bookId: book?.bookId,
|
||||
deletedAt,
|
||||
});
|
||||
response = await tauri.removeChapter(removeChapterId, book?.bookId ?? '', deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>('chapter/remove', {
|
||||
chapterId: removeChapterId,
|
||||
@@ -203,11 +184,7 @@ export default function ScribeChapterComponent() {
|
||||
}, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
|
||||
addToQueue('db:chapter:remove', {
|
||||
chapterId: removeChapterId,
|
||||
bookId: book?.bookId,
|
||||
deletedAt,
|
||||
});
|
||||
addToQueue('remove_chapter', {data: {chapterId: removeChapterId, bookId: book?.bookId, deletedAt}});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
@@ -241,15 +218,16 @@ export default function ScribeChapterComponent() {
|
||||
title: chapterTitle
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook){
|
||||
chapterId = await window.electron.invoke<string>('db:chapter:add', addData);
|
||||
chapterId = await tauri.addChapter({
|
||||
bookId: addData.bookId ?? '',
|
||||
title: addData.title,
|
||||
chapterOrder: addData.chapterOrder,
|
||||
});
|
||||
} else {
|
||||
chapterId = await System.authPostToServer<string>('chapter/add', addData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
|
||||
addToQueue('db:chapter:add', {
|
||||
...addData,
|
||||
chapterId,
|
||||
});
|
||||
addToQueue('add_chapter', {data: {...addData, chapterId}});
|
||||
}
|
||||
}
|
||||
if (!chapterId) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SessionContext } from '@/context/SessionContext';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faLock, faShieldAlt, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface OfflinePinSetupProps {
|
||||
onClose?: () => void;
|
||||
@@ -53,16 +54,14 @@ export default function OfflinePinSetup({ onClose, onSuccess, showOnFirstLogin }
|
||||
setError('');
|
||||
|
||||
try {
|
||||
if (window.electron) {
|
||||
const result = await window.electron.offlinePinSet(pin);
|
||||
const result = await tauri.offlinePinSet(pin);
|
||||
|
||||
if (result.success) {
|
||||
await window.electron.offlineModeSet(true, 30); // 30 days sync interval
|
||||
await tauri.offlineModeSet(true, 30);
|
||||
onSuccess?.();
|
||||
} else {
|
||||
setError(result.error || t('offline.pin.errors.setupFailed'));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[OfflinePin] Error setting PIN:', error);
|
||||
setError(t('offline.pin.errors.setupFailed'));
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useTranslations } from 'next-intl';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faLock, faWifi, faEye, faEyeSlash, faSignOutAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import System from '@/lib/models/System';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface OfflinePinVerifyProps {
|
||||
onSuccess: (userId: string) => void;
|
||||
@@ -29,8 +30,7 @@ export default function OfflinePinVerify({ onSuccess, onCancel }: OfflinePinVeri
|
||||
setError('');
|
||||
|
||||
try {
|
||||
if (window.electron) {
|
||||
const result = await window.electron.offlinePinVerify(pin);
|
||||
const result = await tauri.offlinePinVerify(pin);
|
||||
|
||||
if (result.success && result.userId) {
|
||||
onSuccess(result.userId);
|
||||
@@ -44,7 +44,6 @@ export default function OfflinePinVerify({ onSuccess, onCancel }: OfflinePinVeri
|
||||
setError(result.error || t('offline.pin.verify.incorrect'));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setError(t('offline.pin.verify.error'));
|
||||
} finally {
|
||||
@@ -60,8 +59,8 @@ export default function OfflinePinVerify({ onSuccess, onCancel }: OfflinePinVeri
|
||||
|
||||
const handleLogout = async () => {
|
||||
System.removeCookie("token");
|
||||
await window.electron.removeToken();
|
||||
window.electron.logout();
|
||||
await tauri.removeToken();
|
||||
tauri.logout();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,7 +8,7 @@ import { faWifi, faCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
export default function OfflineToggle() {
|
||||
const { offlineMode, toggleOfflineMode } = useContext(OfflineContext);
|
||||
|
||||
if (!window.electron || !offlineMode.isDatabaseInitialized) {
|
||||
if (!offlineMode.isDatabaseInitialized) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import QuillSense from "@/components/quillsense/QuillSenseComponent";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {faSpinner} from "@fortawesome/free-solid-svg-icons";
|
||||
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
// Lazy loaded Editor components
|
||||
const WorldEditor = lazy(function () {
|
||||
@@ -150,7 +151,7 @@ export default function ComposerRightBar(): React.JSX.Element {
|
||||
badge: t("composerRightBar.homeComponents.facebook.badge"),
|
||||
icon: faFacebook,
|
||||
action: function (): Promise<void> {
|
||||
return window.electron.openExternal('https://www.facebook.com/profile.php?id=61562628720878');
|
||||
return tauri.openExternal('https://www.facebook.com/profile.php?id=61562628720878');
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -160,7 +161,7 @@ export default function ComposerRightBar(): React.JSX.Element {
|
||||
badge: t("composerRightBar.homeComponents.discord.badge"),
|
||||
icon: faDiscord,
|
||||
action: function (): Promise<void> {
|
||||
return window.electron.openExternal('https://discord.gg/CHXRPvmaXm');
|
||||
return tauri.openExternal('https://discord.gg/CHXRPvmaXm');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@@ -17,6 +17,7 @@ import {SyncedBook} from "@/lib/models/SyncedBook";
|
||||
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import {SeriesSyncContext, SeriesSyncContextProps} from "@/context/SeriesSyncContext";
|
||||
import {SyncedSeries, SyncedSeriesBook} from "@/lib/models/SyncedSeries";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface AddNewSeriesFormProps {
|
||||
setCloseForm: Dispatch<SetStateAction<boolean>>;
|
||||
@@ -88,7 +89,7 @@ export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNew
|
||||
let response: string;
|
||||
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<string>('db:series:create', createData);
|
||||
response = await tauri.createSeries(createData);
|
||||
} else {
|
||||
response = await System.authPostToServer<string>(
|
||||
'series/add',
|
||||
|
||||
@@ -23,6 +23,7 @@ import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import {SeriesSyncContext, SeriesSyncContextProps} from "@/context/SeriesSyncContext";
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
|
||||
import {SyncedSeries} from "@/lib/models/SyncedSeries";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface SeriesSettingOption {
|
||||
id: string;
|
||||
@@ -62,7 +63,7 @@ export default function SeriesSettingSidebar(
|
||||
let success: boolean;
|
||||
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:delete', deleteData);
|
||||
success = await tauri.deleteSeries(deleteData.seriesId, deleteData.deletedAt);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>(
|
||||
'series/delete',
|
||||
@@ -72,7 +73,7 @@ export default function SeriesSettingSidebar(
|
||||
);
|
||||
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
|
||||
addToQueue('db:series:delete', deleteData);
|
||||
addToQueue('delete_series', {data: deleteData});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
|
||||
import {SeriesSyncContext, SeriesSyncContextProps} from "@/context/SeriesSyncContext";
|
||||
import {SyncedSeries} from "@/lib/models/SyncedSeries";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
function BasicSeriesInformation(props: object, ref: React.Ref<{ handleSave: () => Promise<void> }>) {
|
||||
const t = useTranslations();
|
||||
@@ -45,7 +46,7 @@ function BasicSeriesInformation(props: object, ref: React.Ref<{ handleSave: () =
|
||||
let response: SeriesDetailResponse;
|
||||
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<SeriesDetailResponse>('db:series:detail', {seriesId});
|
||||
response = await tauri.getSeriesDetail(seriesId);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesDetailResponse>(
|
||||
'series/detail',
|
||||
@@ -90,7 +91,7 @@ function BasicSeriesInformation(props: object, ref: React.Ref<{ handleSave: () =
|
||||
let success: boolean;
|
||||
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:update', updateData);
|
||||
success = await tauri.updateSeries(updateData);
|
||||
} else {
|
||||
const response: SeriesUpdateResponse = await System.authPutToServer<SeriesUpdateResponse>(
|
||||
'series/update',
|
||||
@@ -101,7 +102,7 @@ function BasicSeriesInformation(props: object, ref: React.Ref<{ handleSave: () =
|
||||
success = response.success;
|
||||
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
|
||||
addToQueue('db:series:update', updateData);
|
||||
addToQueue('update_series', {data: updateData});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
|
||||
import {SeriesSyncContext, SeriesSyncContextProps} from "@/context/SeriesSyncContext";
|
||||
import {SyncedSeries, SyncedSeriesBook} from "@/lib/models/SyncedSeries";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Promise<void> }>) {
|
||||
const t = useTranslations();
|
||||
@@ -75,7 +76,7 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
|
||||
let response: SeriesBookProps[];
|
||||
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<SeriesBookProps[]>('db:series:books', {seriesId});
|
||||
response = await tauri.getSeriesBooks(seriesId);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesBookProps[]>(
|
||||
'series/book/list',
|
||||
@@ -123,7 +124,7 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
|
||||
let response: boolean;
|
||||
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<boolean>('db:series:book:add', addData);
|
||||
response = await tauri.addBookToSeries(addData.seriesId, addData.bookId);
|
||||
} else {
|
||||
response = await System.authPostToServer<boolean>(
|
||||
'series/book/add',
|
||||
@@ -133,7 +134,7 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
|
||||
);
|
||||
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
|
||||
addToQueue('db:series:book:add', addData);
|
||||
addToQueue('add_book_to_series', {data: addData});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +181,7 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
|
||||
let response: boolean;
|
||||
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<boolean>('db:series:book:remove', removeData);
|
||||
response = await tauri.removeBookFromSeries(removeData.seriesId, removeData.bookId, removeData.deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>(
|
||||
'series/book/remove',
|
||||
@@ -190,7 +191,7 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
|
||||
);
|
||||
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
|
||||
addToQueue('db:series:book:remove', removeData);
|
||||
addToQueue('remove_book_from_series', {data: removeData});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +248,7 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
|
||||
let response: boolean;
|
||||
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<boolean>('db:series:book:reorder', reorderData);
|
||||
response = await tauri.reorderSeriesBooks(reorderData.seriesId, reorderData.booksOrder);
|
||||
} else {
|
||||
response = await System.authPutToServer<boolean>(
|
||||
'series/book/reorder',
|
||||
@@ -257,7 +258,7 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
|
||||
);
|
||||
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
|
||||
addToQueue('db:series:book:reorder', reorderData);
|
||||
addToQueue('reorder_series_books', {data: reorderData});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React, { useState, useEffect, useCallback, ReactNode } from 'react';
|
||||
import OfflineContext, { OfflineMode, defaultOfflineMode } from './OfflineContext';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface OfflineProviderProps {
|
||||
children: ReactNode;
|
||||
@@ -12,30 +13,17 @@ export default function OfflineProvider({ children }: OfflineProviderProps) {
|
||||
|
||||
const initializeDatabase = useCallback(async (userId: string, encryptionKey?: string): Promise<boolean> => {
|
||||
try {
|
||||
if (typeof window === 'undefined' || !(window as any).electron) {
|
||||
console.warn('Not running in Electron, offline mode not available');
|
||||
return false;
|
||||
}
|
||||
|
||||
let userKey = encryptionKey;
|
||||
if (!userKey) {
|
||||
const storedKey = await (window as any).electron.getUserEncryptionKey(userId);
|
||||
const storedKey = await tauri.getUserEncryptionKey(userId);
|
||||
if (storedKey) {
|
||||
userKey = storedKey;
|
||||
} else {
|
||||
const keyResult = await (window as any).electron.generateEncryptionKey(userId);
|
||||
if (!keyResult.success) {
|
||||
throw new Error(keyResult.error || 'Failed to generate encryption key');
|
||||
}
|
||||
userKey = keyResult.key;
|
||||
await (window as any).electron.setUserEncryptionKey(userId, userKey);
|
||||
throw new Error('No encryption key found for user');
|
||||
}
|
||||
}
|
||||
|
||||
const result = await (window as any).electron.dbInitialize(userId, userKey);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Failed to initialize database');
|
||||
}
|
||||
await tauri.dbInitialize(userId, userKey);
|
||||
|
||||
setOfflineMode(prev => ({
|
||||
...prev,
|
||||
@@ -45,7 +33,7 @@ export default function OfflineProvider({ children }: OfflineProviderProps) {
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize database:', error);
|
||||
console.error('Failed to initialize database:', error, 'userId:', userId, 'hasKey:', !!userKey);
|
||||
setOfflineMode(prev => ({
|
||||
...prev,
|
||||
isDatabaseInitialized: false,
|
||||
|
||||
65
electron.d.ts
vendored
65
electron.d.ts
vendored
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* TypeScript declarations for window.electron API
|
||||
* Must match exactly with electron/preload.ts
|
||||
*
|
||||
* Usage:
|
||||
* - Use invoke<T>(channel, ...args) for all IPC calls
|
||||
* - Shortcuts are provided for common operations (tokens, lang, encryption)
|
||||
*/
|
||||
export interface IElectronAPI {
|
||||
// Platform info
|
||||
platform: NodeJS.Platform;
|
||||
|
||||
// Generic invoke method - use this for all IPC calls
|
||||
invoke: <T>(channel: string, ...args: any[]) => Promise<T>;
|
||||
|
||||
// Token management (shortcuts for convenience)
|
||||
getToken: () => Promise<string | null>;
|
||||
setToken: (token: string) => Promise<void>;
|
||||
removeToken: () => Promise<void>;
|
||||
|
||||
// Language management (shortcuts for convenience)
|
||||
getLang: () => Promise<'fr' | 'en'>;
|
||||
setLang: (lang: 'fr' | 'en') => Promise<void>;
|
||||
|
||||
// Auth events (one-way communication)
|
||||
loginSuccess: (token: string) => void;
|
||||
logout: () => void;
|
||||
|
||||
// User initialization (after getting user info from server)
|
||||
initUser: (userId: string) => Promise<{ success: boolean; keyCreated?: boolean; error?: string }>;
|
||||
|
||||
// Encryption key management (shortcuts for convenience)
|
||||
generateEncryptionKey: (userId: string) => Promise<string>;
|
||||
getUserEncryptionKey: (userId: string) => Promise<string | null>;
|
||||
setUserEncryptionKey: (userId: string, encryptionKey: string) => Promise<void>;
|
||||
|
||||
// Database initialization (shortcut for convenience)
|
||||
dbInitialize: (userId: string, encryptionKey: string) => Promise<boolean>;
|
||||
|
||||
// Open external links (browser/native app)
|
||||
openExternal: (url: string) => Promise<void>;
|
||||
|
||||
// OAuth login via BrowserWindow
|
||||
oauthLogin: (provider: 'google' | 'facebook' | 'apple', baseUrl: string) => Promise<{
|
||||
success: boolean;
|
||||
code?: string;
|
||||
state?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
|
||||
// Offline mode management
|
||||
offlinePinSet: (pin: string) => Promise<{ success: boolean; error?: string }>;
|
||||
offlinePinVerify: (pin: string) => Promise<{ success: boolean; userId?: string; error?: string }>;
|
||||
offlineModeSet: (enabled: boolean, syncInterval?: number) => Promise<{ success: boolean }>;
|
||||
offlineModeGet: () => Promise<{ enabled: boolean; syncInterval: number; hasPin: boolean; lastUserId?: string }>;
|
||||
offlineSyncCheck: () => Promise<{ shouldSync: boolean; daysSinceSync?: number; syncInterval?: number }>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: IElectronAPI;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -2,6 +2,7 @@
|
||||
import {useCallback, useContext, useEffect, useState} from 'react';
|
||||
import {Attribute, CharacterListResponse, CharacterProps} from '@/lib/models/Character';
|
||||
import {SeriesCharacterProps} from '@/lib/models/Series';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import {SessionContext} from '@/context/SessionContext';
|
||||
import {BookContext} from '@/context/BookContext';
|
||||
import {AlertContext} from '@/context/AlertContext';
|
||||
@@ -147,10 +148,7 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
|
||||
let response: SeriesCharacterProps[];
|
||||
// Dual logic: offline ou livre local → IPC, sinon serveur
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<SeriesCharacterProps[]>(
|
||||
'db:series:character:list',
|
||||
{seriesId: bookSeriesId}
|
||||
);
|
||||
response = await tauri.getSeriesCharacterList(bookSeriesId);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesCharacterProps[]>(
|
||||
'series/character/list',
|
||||
@@ -176,10 +174,7 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
|
||||
// Series mode - dual logic
|
||||
let response: SeriesCharacterProps[];
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<SeriesCharacterProps[]>(
|
||||
'db:series:character:list',
|
||||
{seriesId: entityId}
|
||||
);
|
||||
response = await tauri.getSeriesCharacterList(entityId);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesCharacterProps[]>(
|
||||
'series/character/list',
|
||||
@@ -231,11 +226,11 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
|
||||
// Pattern B: GET dans contexte livre
|
||||
let response: CharacterListResponse;
|
||||
if (isCurrentlyOffline()) {
|
||||
// Offline → IPC
|
||||
response = await window.electron.invoke<CharacterListResponse>('db:character:list', {bookid: entityId});
|
||||
// Offline → Tauri
|
||||
response = await tauri.getCharacterList(entityId, true) as CharacterListResponse;
|
||||
} else if (book?.localBook) {
|
||||
// Online mais livre local → IPC
|
||||
response = await window.electron.invoke<CharacterListResponse>('db:character:list', {bookid: entityId});
|
||||
// Online mais livre local → Tauri
|
||||
response = await tauri.getCharacterList(entityId, true) as CharacterListResponse;
|
||||
} else {
|
||||
// Online + livre serveur → Server
|
||||
response = await System.authGetQueryToServer<CharacterListResponse>(
|
||||
@@ -306,15 +301,15 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
|
||||
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Offline OU livre local → IPC
|
||||
response = await window.electron.invoke<boolean>('db:book:tool:update', requestData);
|
||||
// Offline OU livre local → Tauri
|
||||
response = await tauri.updateBookToolSetting(requestData.bookId, requestData.toolName, requestData.enabled);
|
||||
} else {
|
||||
// Online + livre serveur → Server
|
||||
response = await System.authPatchToServer<boolean>('book/tool-setting', requestData, userToken, lang);
|
||||
|
||||
// Si le livre a une copie locale → addToQueue pour sync
|
||||
if (book?.bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book.bookId)) {
|
||||
addToQueue('db:book:tool:update', requestData);
|
||||
addToQueue('update_book_tool_setting', {data: requestData});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,7 +380,7 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
|
||||
}
|
||||
};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
characterId = await window.electron.invoke<string>('db:series:character:add', seriesCharacterData);
|
||||
characterId = await tauri.addSeriesCharacter(seriesCharacterData);
|
||||
} else {
|
||||
characterId = await System.authPostToServer<string>(
|
||||
'series/character/add',
|
||||
@@ -394,7 +389,7 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
|
||||
lang
|
||||
);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:character:add', {...seriesCharacterData, id: characterId});
|
||||
addToQueue('add_series_character', {data: {...seriesCharacterData, id: characterId}});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -405,15 +400,15 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
|
||||
};
|
||||
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Offline OU livre local → IPC
|
||||
characterId = await window.electron.invoke<string>('db:character:create', requestData);
|
||||
// Offline OU livre local → Tauri
|
||||
characterId = await tauri.createCharacter(requestData.character, requestData.bookId, requestData.id);
|
||||
} else {
|
||||
// Online + livre serveur → Server
|
||||
characterId = await System.authPostToServer<string>('character/add', requestData, userToken, lang);
|
||||
|
||||
// Si le livre a une copie locale → addToQueue pour sync
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:character:create', {...requestData, id: characterId});
|
||||
addToQueue('create_character', {data: {...requestData, id: characterId}});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -470,11 +465,11 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
|
||||
}
|
||||
};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<boolean>('db:series:character:update', updateData);
|
||||
response = await tauri.updateSeriesCharacter(updateData);
|
||||
} else {
|
||||
response = await System.authPatchToServer<boolean>('series/character/update', updateData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:character:update', updateData);
|
||||
addToQueue('update_series_character', {data: updateData});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -484,15 +479,15 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
|
||||
};
|
||||
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Offline OU livre local → IPC
|
||||
response = await window.electron.invoke<boolean>('db:character:update', requestData);
|
||||
// Offline OU livre local → Tauri
|
||||
response = await tauri.updateCharacter(requestData.character);
|
||||
} else {
|
||||
// Online + livre serveur → Server
|
||||
response = await System.authPostToServer<boolean>('character/update', requestData, userToken, lang);
|
||||
|
||||
// Si le livre a une copie locale → addToQueue pour sync
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:character:update', requestData);
|
||||
addToQueue('update_character', {data: requestData});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -529,26 +524,26 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
|
||||
// Series mode - dual logic
|
||||
const requestData = {characterId, deletedAt};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<boolean>('db:series:character:delete', requestData);
|
||||
response = await tauri.deleteSeriesCharacter(requestData.characterId, requestData.deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>('series/character/delete', requestData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:character:delete', requestData);
|
||||
addToQueue('delete_series_character', {data: requestData});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pattern A: mutations
|
||||
const requestData = {characterId, bookId: entityId, deletedAt};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Offline OU livre local → IPC
|
||||
response = await window.electron.invoke<boolean>('db:character:delete', requestData);
|
||||
// Offline OU livre local → Tauri
|
||||
response = await tauri.deleteCharacter(requestData.characterId, requestData.bookId, requestData.deletedAt);
|
||||
} else {
|
||||
// Online + livre serveur → Server
|
||||
response = await System.authDeleteToServer<boolean>('character/delete', requestData, userToken, lang);
|
||||
|
||||
// Si le livre a une copie locale → addToQueue pour sync
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:character:delete', requestData);
|
||||
addToQueue('delete_character', {data: requestData});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -595,25 +590,25 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
attributeId = await window.electron.invoke<string>('db:series:character:attribute:add', requestData);
|
||||
attributeId = await tauri.addSeriesCharacterAttribute(requestData);
|
||||
} else {
|
||||
attributeId = await System.authPostToServer<string>('series/character/attribute/add', requestData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:character:attribute:add', {...requestData, id: attributeId});
|
||||
addToQueue('add_series_character_attribute', {data: {...requestData, id: attributeId}});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pattern A: mutations
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Offline OU livre local → IPC
|
||||
attributeId = await window.electron.invoke<string>('db:character:attribute:add', requestData);
|
||||
// Offline OU livre local → Tauri
|
||||
attributeId = await tauri.addCharacterAttribute(requestData.characterId, requestData.type, requestData.name, requestData.id);
|
||||
} else {
|
||||
// Online + livre serveur → Server
|
||||
attributeId = await System.authPostToServer<string>('character/attribute/add', requestData, userToken, lang);
|
||||
|
||||
// Si le livre a une copie locale → addToQueue pour sync
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:character:attribute:add', {...requestData, id: attributeId});
|
||||
addToQueue('add_character_attribute', {data: {...requestData, id: attributeId}});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -658,26 +653,26 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
|
||||
// Series mode - dual logic
|
||||
const requestData = {attributeId: attrId, deletedAt};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<boolean>('db:series:character:attribute:delete', requestData);
|
||||
response = await tauri.deleteSeriesCharacterAttribute(requestData.attributeId, requestData.deletedAt);
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>('series/character/attribute/delete', requestData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:character:attribute:delete', requestData);
|
||||
addToQueue('delete_series_character_attribute', {data: requestData});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pattern A: mutations
|
||||
const requestData = {attributeId: attrId, bookId: entityId, deletedAt};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Offline OU livre local → IPC
|
||||
response = await window.electron.invoke<boolean>('db:character:attribute:delete', requestData);
|
||||
// Offline OU livre local → Tauri
|
||||
response = await tauri.deleteCharacterAttribute(requestData.attributeId, requestData.bookId, requestData.deletedAt);
|
||||
} else {
|
||||
// Online + livre serveur → Server
|
||||
response = await System.authDeleteToServer<boolean>('character/attribute/delete', requestData, userToken, lang);
|
||||
|
||||
// Si le livre a une copie locale → addToQueue pour sync
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:character:attribute:delete', requestData);
|
||||
addToQueue('delete_character_attribute', {data: requestData});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -734,8 +729,8 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
|
||||
|
||||
let seriesCharacterId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
seriesCharacterId = await window.electron.invoke<string>('db:series:character:add', seriesCharacterData);
|
||||
// Mode offline ou livre local → Tauri
|
||||
seriesCharacterId = await tauri.addSeriesCharacter(seriesCharacterData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
seriesCharacterId = await System.authPostToServer<string>(
|
||||
@@ -746,7 +741,7 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
|
||||
);
|
||||
// Si la série a une copie locale → addToQueue
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === bookSeriesId)) {
|
||||
addToQueue('db:series:character:add', {...seriesCharacterData, id: seriesCharacterId});
|
||||
addToQueue('add_series_character', {data: {...seriesCharacterData, id: seriesCharacterId}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -760,14 +755,14 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
|
||||
|
||||
let updateResponse: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
updateResponse = await window.electron.invoke<boolean>('db:character:update', updateData);
|
||||
// Mode offline ou livre local → Tauri
|
||||
updateResponse = await tauri.updateCharacter(updateData.character);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
updateResponse = await System.authPostToServer<boolean>('character/update', updateData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:character:update', updateData);
|
||||
addToQueue('update_character', {data: updateData});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -869,14 +864,14 @@ export function useCharacters(config: UseCharactersConfig): UseCharactersReturn
|
||||
|
||||
let characterId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
characterId = await window.electron.invoke<string>('db:character:create', requestData);
|
||||
// Mode offline ou livre local → Tauri
|
||||
characterId = await tauri.createCharacter(requestData.character, requestData.bookId, requestData.id);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
characterId = await System.authPostToServer<string>('character/add', requestData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:character:create', {...requestData, id: characterId});
|
||||
addToQueue('create_character', {data: {...requestData, id: characterId}});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {SyncedBook} from '@/lib/models/SyncedBook';
|
||||
import {SeriesContext, SeriesContextProps} from '@/context/SeriesContext';
|
||||
import {SeriesSyncContext, SeriesSyncContextProps} from '@/context/SeriesSyncContext';
|
||||
import {SyncedSeries} from '@/lib/models/SyncedSeries';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
export interface SubElement {
|
||||
id: string;
|
||||
@@ -142,10 +143,7 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
let response: SeriesLocationItem[];
|
||||
// Dual logic: offline ou livre local → IPC, sinon serveur
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<SeriesLocationItem[]>(
|
||||
'db:series:location:list',
|
||||
{seriesId: bookSeriesId}
|
||||
);
|
||||
response = await tauri.getSeriesLocationList(bookSeriesId);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesLocationItem[]>(
|
||||
'series/location/list',
|
||||
@@ -171,10 +169,7 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
// Series mode - dual logic
|
||||
let response: SeriesLocationItem[];
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<SeriesLocationItem[]>(
|
||||
'db:series:location:list',
|
||||
{seriesId: entityId}
|
||||
);
|
||||
response = await tauri.getSeriesLocationList(entityId);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesLocationItem[]>(
|
||||
'series/location/list',
|
||||
@@ -209,9 +204,9 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
} else {
|
||||
let response: LocationListResponse;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<LocationListResponse>('db:location:all', {bookid: entityId});
|
||||
response = await tauri.getAllLocations(entityId, true) as unknown as LocationListResponse;
|
||||
} else if (book?.localBook) {
|
||||
response = await window.electron.invoke<LocationListResponse>('db:location:all', {bookid: entityId});
|
||||
response = await tauri.getAllLocations(entityId, true) as unknown as LocationListResponse;
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<LocationListResponse>(
|
||||
'location/all',
|
||||
@@ -257,12 +252,12 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
};
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:tool:update', requestData);
|
||||
response = await tauri.updateBookToolSetting(requestData.bookId!, requestData.toolName, requestData.enabled);
|
||||
} else {
|
||||
response = await System.authPatchToServer<boolean>('book/tool-setting', requestData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) {
|
||||
addToQueue('db:book:tool:update', requestData);
|
||||
addToQueue('update_book_tool_setting', {data: requestData});
|
||||
}
|
||||
}
|
||||
if (response && setBook && book) {
|
||||
@@ -300,7 +295,7 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
name: newSectionName,
|
||||
};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
sectionId = await window.electron.invoke<string>('db:series:location:section:add', addData);
|
||||
sectionId = await tauri.addSeriesLocationSection(addData);
|
||||
} else {
|
||||
sectionId = await System.authPostToServer<string>(
|
||||
'series/location/section/add',
|
||||
@@ -309,7 +304,7 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
lang
|
||||
);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:location:section:add', {...addData, id: sectionId});
|
||||
addToQueue('add_series_location_section', {data: {...addData, id: sectionId}});
|
||||
}
|
||||
}
|
||||
if (!sectionId) {
|
||||
@@ -322,12 +317,12 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
locationName: newSectionName,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
sectionId = await window.electron.invoke<string>('db:location:section:add', requestData);
|
||||
sectionId = await tauri.addLocationSection(requestData.locationName, requestData.bookId, undefined, undefined);
|
||||
} else {
|
||||
sectionId = await System.authPostToServer<string>('location/section/add', requestData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:section:add', {...requestData, id: sectionId});
|
||||
addToQueue('add_location_section', {data: {...requestData, id: sectionId}});
|
||||
}
|
||||
}
|
||||
if (!sectionId) {
|
||||
@@ -371,7 +366,7 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
name: newElementNames[sectionId],
|
||||
};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
elementId = await window.electron.invoke<string>('db:series:location:element:add', addData);
|
||||
elementId = await tauri.addSeriesLocationElement(addData);
|
||||
} else {
|
||||
elementId = await System.authPostToServer<string>(
|
||||
'series/location/element/add',
|
||||
@@ -380,7 +375,7 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
lang
|
||||
);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:location:element:add', {...addData, id: elementId});
|
||||
addToQueue('add_series_location_element', {data: {...addData, id: elementId}});
|
||||
}
|
||||
}
|
||||
if (!elementId) {
|
||||
@@ -394,12 +389,12 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
elementName: newElementNames[sectionId],
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
elementId = await window.electron.invoke<string>('db:location:element:add', requestData);
|
||||
elementId = await tauri.addLocationElement(requestData.locationId, requestData.elementName, undefined);
|
||||
} else {
|
||||
elementId = await System.authPostToServer<string>('location/element/add', requestData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:element:add', {...requestData, id: elementId});
|
||||
addToQueue('add_location_element', {data: {...requestData, id: elementId}});
|
||||
}
|
||||
}
|
||||
if (!elementId) {
|
||||
@@ -453,7 +448,7 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
name: newSubElementNames[elementIndex],
|
||||
};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
subElementId = await window.electron.invoke<string>('db:series:location:subelement:add', addData);
|
||||
subElementId = await tauri.addSeriesLocationSubElement(addData);
|
||||
} else {
|
||||
subElementId = await System.authPostToServer<string>(
|
||||
'series/location/sub-element/add',
|
||||
@@ -462,7 +457,7 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
lang
|
||||
);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:location:subelement:add', {...addData, id: subElementId});
|
||||
addToQueue('add_series_location_sub_element', {data: {...addData, id: subElementId}});
|
||||
}
|
||||
}
|
||||
if (!subElementId) {
|
||||
@@ -475,12 +470,12 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
subElementName: newSubElementNames[elementIndex],
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
subElementId = await window.electron.invoke<string>('db:location:subelement:add', requestData);
|
||||
subElementId = await tauri.addLocationSubElement(requestData.elementId, requestData.subElementName, undefined);
|
||||
} else {
|
||||
subElementId = await System.authPostToServer<string>('location/sub-element/add', requestData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:subelement:add', {...requestData, id: subElementId});
|
||||
addToQueue('add_location_sub_element', {data: {...requestData, id: subElementId}});
|
||||
}
|
||||
}
|
||||
if (!subElementId) {
|
||||
@@ -519,11 +514,11 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
// Series mode - dual logic
|
||||
const deleteData = {locationId: sectionId, deletedAt};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:location:delete', deleteData);
|
||||
success = await tauri.deleteSeriesLocation(deleteData.locationId, deleteData.deletedAt);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('series/location/delete', deleteData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:location:delete', deleteData);
|
||||
addToQueue('delete_series_location', {data: deleteData});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -531,12 +526,12 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
locationId: sectionId, bookId: entityId, deletedAt,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:location:delete', requestData);
|
||||
success = await tauri.deleteLocationSection(requestData.locationId, requestData.bookId, requestData.deletedAt);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('location/delete', requestData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:delete', requestData);
|
||||
addToQueue('delete_location_section', {data: requestData});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -569,11 +564,11 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
// Series mode - dual logic
|
||||
const deleteData = {elementId, deletedAt};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:location:element:delete', deleteData);
|
||||
success = await tauri.deleteSeriesLocationElement(deleteData.elementId!, deleteData.deletedAt);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('series/location/element/delete', deleteData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:location:element:delete', deleteData);
|
||||
addToQueue('delete_series_location_element', {data: deleteData});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -581,12 +576,12 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
elementId, bookId: entityId, deletedAt,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:location:element:delete', requestData);
|
||||
success = await tauri.deleteLocationElement(requestData.elementId!, requestData.bookId, requestData.deletedAt);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('location/element/delete', requestData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:element:delete', requestData);
|
||||
addToQueue('delete_location_element', {data: requestData});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -622,11 +617,11 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
// Series mode - dual logic
|
||||
const deleteData = {subElementId, deletedAt};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:location:subelement:delete', deleteData);
|
||||
success = await tauri.deleteSeriesLocationSubElement(deleteData.subElementId!, deleteData.deletedAt);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('series/location/sub-element/delete', deleteData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:location:subelement:delete', deleteData);
|
||||
addToQueue('delete_series_location_sub_element', {data: deleteData});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -634,12 +629,12 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
subElementId, bookId: entityId, deletedAt,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:location:subelement:delete', requestData);
|
||||
success = await tauri.deleteLocationSubElement(requestData.subElementId!, requestData.bookId, requestData.deletedAt);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('location/sub-element/delete', requestData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:subelement:delete', requestData);
|
||||
addToQueue('delete_location_sub_element', {data: requestData});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -694,12 +689,12 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
};
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:location:update', requestData);
|
||||
response = await tauri.updateLocations(requestData.locations);
|
||||
} else {
|
||||
response = await System.authPostToServer<boolean>('location/update', requestData, userToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:update', requestData);
|
||||
addToQueue('update_locations', {data: requestData});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
@@ -729,14 +724,14 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
|
||||
let seriesLocationId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
seriesLocationId = await window.electron.invoke<string>('db:series:location:section:add', seriesLocationData);
|
||||
// Mode offline ou livre local → Tauri
|
||||
seriesLocationId = await tauri.addSeriesLocationSection(seriesLocationData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
seriesLocationId = await System.authPostToServer<string>('series/location/section/add', seriesLocationData, userToken, lang);
|
||||
// Si la série a une copie locale → addToQueue avec l'ID du serveur
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === bookSeriesId)) {
|
||||
addToQueue('db:series:location:section:add', {...seriesLocationData, id: seriesLocationId});
|
||||
addToQueue('add_series_location_section', {data: {...seriesLocationData, id: seriesLocationId}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -749,14 +744,14 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
|
||||
let updateResponse: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
updateResponse = await window.electron.invoke<boolean>('db:location:section:update', updateData);
|
||||
// Mode offline ou livre local → Tauri
|
||||
updateResponse = await tauri.updateLocationSectionWithSeriesLink(updateData.sectionId, updateData.sectionName, updateData.seriesLocationId);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
updateResponse = await System.authPostToServer<boolean>('location/section/update', updateData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:section:update', updateData);
|
||||
addToQueue('update_location_section_with_series_link', {data: updateData});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -799,14 +794,14 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
|
||||
let sectionId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
sectionId = await window.electron.invoke<string>('db:location:section:add', sectionData);
|
||||
// Mode offline ou livre local → Tauri
|
||||
sectionId = await tauri.addLocationSection(sectionData.locationName, sectionData.bookId, undefined, sectionData.seriesLocationId);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
sectionId = await System.authPostToServer<string>('location/section/add', sectionData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue avec l'ID du serveur
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:section:add', {...sectionData, id: sectionId});
|
||||
addToQueue('add_location_section', {data: {...sectionData, id: sectionId}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -826,14 +821,14 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
|
||||
let elementId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
elementId = await window.electron.invoke<string>('db:location:element:add', elementData);
|
||||
// Mode offline ou livre local → Tauri
|
||||
elementId = await tauri.addLocationElement(elementData.locationId, elementData.elementName, undefined);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
elementId = await System.authPostToServer<string>('location/element/add', elementData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue avec l'ID du serveur
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:element:add', {...elementData, id: elementId});
|
||||
addToQueue('add_location_element', {data: {...elementData, id: elementId}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -849,14 +844,14 @@ export function useLocations(config: UseLocationsConfig): UseLocationsReturn {
|
||||
|
||||
let subElementId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
subElementId = await window.electron.invoke<string>('db:location:subelement:add', subElementData);
|
||||
// Mode offline ou livre local → Tauri
|
||||
subElementId = await tauri.addLocationSubElement(subElementData.elementId, subElementData.subElementName, undefined);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
subElementId = await System.authPostToServer<string>('location/sub-element/add', subElementData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue avec l'ID du serveur
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:location:subelement:add', {...subElementData, id: subElementId});
|
||||
addToQueue('add_location_sub_element', {data: {...subElementData, id: subElementId}});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {SyncedBook} from '@/lib/models/SyncedBook';
|
||||
import {SeriesContext, SeriesContextProps} from '@/context/SeriesContext';
|
||||
import {SeriesSyncContext, SeriesSyncContextProps} from '@/context/SeriesSyncContext';
|
||||
import {SyncedSeries} from '@/lib/models/SyncedSeries';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
export interface UseSpellsConfig {
|
||||
entityType: 'book' | 'series';
|
||||
@@ -113,10 +114,7 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
let response: SeriesSpellListResponse;
|
||||
// Dual logic: offline ou livre local → IPC, sinon serveur
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<SeriesSpellListResponse>(
|
||||
'db:series:spell:list',
|
||||
{seriesId: bookSeriesId}
|
||||
);
|
||||
response = await tauri.getSeriesSpellList(bookSeriesId) as SeriesSpellListResponse;
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesSpellListResponse>(
|
||||
'series/spell/list',
|
||||
@@ -142,10 +140,7 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
// Series mode - dual logic
|
||||
let response: SeriesSpellListResponse;
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<SeriesSpellListResponse>(
|
||||
'db:series:spell:list',
|
||||
{seriesId: entityId}
|
||||
);
|
||||
response = await tauri.getSeriesSpellList(entityId) as SeriesSpellListResponse;
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesSpellListResponse>(
|
||||
'series/spell/list',
|
||||
@@ -174,9 +169,9 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
} else {
|
||||
let response: SpellListResponse;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<SpellListResponse>('db:spell:list', {bookid: entityId});
|
||||
response = await tauri.getSpellList(entityId, true) as SpellListResponse;
|
||||
} else if (book?.localBook) {
|
||||
response = await window.electron.invoke<SpellListResponse>('db:spell:list', {bookid: entityId});
|
||||
response = await tauri.getSpellList(entityId, true) as SpellListResponse;
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SpellListResponse>(
|
||||
'spell/list',
|
||||
@@ -242,10 +237,7 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
// Series mode - dual logic
|
||||
let response: SeriesSpellDetailResponse;
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<SeriesSpellDetailResponse>(
|
||||
'db:series:spell:detail',
|
||||
{spellId: spell.id}
|
||||
);
|
||||
response = await tauri.getSeriesSpellDetail(spell.id) as SeriesSpellDetailResponse;
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesSpellDetailResponse>(
|
||||
'series/spell/detail',
|
||||
@@ -270,9 +262,9 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
} else {
|
||||
let response: SpellProps;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<SpellProps>('db:spell:detail', {spellid: spell.id});
|
||||
response = await tauri.getSpellDetail(spell.id) as SpellProps;
|
||||
} else if (book?.localBook) {
|
||||
response = await window.electron.invoke<SpellProps>('db:spell:detail', {spellid: spell.id});
|
||||
response = await tauri.getSpellDetail(spell.id) as SpellProps;
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SpellProps>(
|
||||
'spell/detail',
|
||||
@@ -298,10 +290,7 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
if (response.seriesSpellId) {
|
||||
let seriesSpellResponse: SeriesSpellDetailResponse;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
seriesSpellResponse = await window.electron.invoke<SeriesSpellDetailResponse>(
|
||||
'db:series:spell:detail',
|
||||
{spellId: response.seriesSpellId}
|
||||
);
|
||||
seriesSpellResponse = await tauri.getSeriesSpellDetail(response.seriesSpellId) as SeriesSpellDetailResponse;
|
||||
} else {
|
||||
seriesSpellResponse = await System.authGetQueryToServer<SeriesSpellDetailResponse>(
|
||||
'series/spell/detail',
|
||||
@@ -353,11 +342,11 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
};
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:tool:update', requestData);
|
||||
response = await tauri.updateBookToolSetting(requestData.bookId, requestData.toolName, requestData.enabled);
|
||||
} else {
|
||||
response = await System.authPatchToServer<boolean>('book/tool-setting', requestData, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) {
|
||||
addToQueue('db:book:tool:update', requestData);
|
||||
addToQueue('update_book_tool_setting', {data: requestData});
|
||||
}
|
||||
}
|
||||
if (response && setBook && book) {
|
||||
@@ -410,11 +399,11 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
notes: spell.notes,
|
||||
};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
newSpellId = await window.electron.invoke<string>('db:series:spell:add', data);
|
||||
newSpellId = await tauri.addSeriesSpell(data);
|
||||
} else {
|
||||
newSpellId = await System.authPostToServer<string>('series/spell/add', data, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:spell:add', {...data, id: newSpellId});
|
||||
addToQueue('add_series_spell', {data: {...data, id: newSpellId}});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -432,11 +421,11 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
}
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
newSpellId = await window.electron.invoke<string>('db:spell:create', data);
|
||||
newSpellId = await tauri.createSpell(data.bookId, data.spell);
|
||||
} else {
|
||||
newSpellId = await System.authPostToServer<string>('spell/add', data, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:spell:create', {...data, id: newSpellId});
|
||||
addToQueue('create_spell', {data: {...data, id: newSpellId}});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -492,20 +481,20 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:spell:update', data);
|
||||
success = await tauri.updateSeriesSpell(data);
|
||||
} else {
|
||||
success = await System.authPutToServer<boolean>('series/spell/update', data, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:spell:update', data);
|
||||
addToQueue('update_series_spell', {data});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:spell:update', data);
|
||||
success = await tauri.updateSpell(data.id, data);
|
||||
} else {
|
||||
success = await System.authPutToServer<boolean>('spell/update', data, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:spell:update', data);
|
||||
addToQueue('update_spell', {data});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -549,21 +538,21 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
// Series mode - dual logic
|
||||
const requestData = {spellId, deletedAt};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:spell:delete', requestData);
|
||||
success = await tauri.deleteSeriesSpell(requestData.spellId, requestData.deletedAt);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('series/spell/delete', requestData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:spell:delete', requestData);
|
||||
addToQueue('delete_series_spell', {data: requestData});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const requestData = {spellId, bookId: entityId, deletedAt};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:spell:delete', requestData);
|
||||
success = await tauri.deleteSpell(requestData.spellId, requestData.bookId, requestData.deletedAt);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('spell/delete', requestData, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:spell:delete', requestData);
|
||||
addToQueue('delete_spell', {data: requestData});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -605,7 +594,7 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
let seriesSpellId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
seriesSpellId = await window.electron.invoke<string>('db:series:spell:add', seriesSpellData);
|
||||
seriesSpellId = await tauri.addSeriesSpell(seriesSpellData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
seriesSpellId = await System.authPostToServer<string>(
|
||||
@@ -616,7 +605,7 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
);
|
||||
// Si la série a une copie locale → addToQueue
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === bookSeriesId)) {
|
||||
addToQueue('db:series:spell:add', {...seriesSpellData, id: seriesSpellId});
|
||||
addToQueue('add_series_spell', {data: {...seriesSpellData, id: seriesSpellId}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,14 +625,14 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
|
||||
let updateSuccess: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
updateSuccess = await window.electron.invoke<boolean>('db:spell:update', updateData);
|
||||
// Mode offline ou livre local → Tauri
|
||||
updateSuccess = await tauri.updateSpell(updateData.id, updateData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
updateSuccess = await System.authPutToServer<boolean>('spell/update', updateData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:spell:update', updateData);
|
||||
addToQueue('update_spell', {data: updateData});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,11 +669,8 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
// 1. Récupérer les détails du sort de la série
|
||||
let seriesSpellDetail: SeriesSpellDetailResponse;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline → IPC pour récupérer les détails du sort de la série locale
|
||||
seriesSpellDetail = await window.electron.invoke<SeriesSpellDetailResponse>(
|
||||
'db:series:spell:detail',
|
||||
{spellId: seriesSpellId}
|
||||
);
|
||||
// Mode offline → Tauri pour récupérer les détails du sort de la série locale
|
||||
seriesSpellDetail = await tauri.getSeriesSpellDetail(seriesSpellId) as SeriesSpellDetailResponse;
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
seriesSpellDetail = await System.authGetQueryToServer<SeriesSpellDetailResponse>(
|
||||
@@ -715,14 +701,14 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
|
||||
let createdSpellId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
createdSpellId = await window.electron.invoke<string>('db:spell:create', spellData);
|
||||
// Mode offline ou livre local → Tauri
|
||||
createdSpellId = await tauri.createSpell(spellData.bookId, spellData.spell);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
createdSpellId = await System.authPostToServer<string>('spell/add', spellData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:spell:create', {...spellData, id: createdSpellId});
|
||||
addToQueue('create_spell', {data: {...spellData, id: createdSpellId}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,7 +744,7 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
};
|
||||
let tagId: string;
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
tagId = await window.electron.invoke<string>('db:series:spell:tag:add', addData);
|
||||
tagId = await tauri.addSeriesSpellTag(addData);
|
||||
} else {
|
||||
tagId = await System.authPostToServer<string>(
|
||||
'series/spell/tag/add',
|
||||
@@ -767,7 +753,7 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
lang
|
||||
);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:spell:tag:add', {...addData, id: tagId});
|
||||
addToQueue('add_series_spell_tag', {data: {...addData, id: tagId}});
|
||||
}
|
||||
}
|
||||
if (tagId) {
|
||||
@@ -786,11 +772,11 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
};
|
||||
let newTag: SpellTagProps;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
newTag = await window.electron.invoke<SpellTagProps>('db:spell:tag:create', requestData);
|
||||
newTag = await tauri.createSpellTag(requestData.bookId, requestData.name, requestData.color) as SpellTagProps;
|
||||
} else {
|
||||
newTag = await System.authPostToServer<SpellTagProps>('spell/tag/add', requestData, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:spell:tag:create', {...requestData, id: newTag?.id});
|
||||
addToQueue('create_spell_tag', {data: {...requestData, id: newTag?.id}});
|
||||
}
|
||||
}
|
||||
if (newTag && newTag.id) {
|
||||
@@ -816,20 +802,20 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
if (isSeriesMode) {
|
||||
// Series mode - dual logic
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:spell:tag:update', requestData);
|
||||
success = await tauri.updateSeriesSpellTag(requestData);
|
||||
} else {
|
||||
success = await System.authPutToServer<boolean>('series/spell/tag/update', requestData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:spell:tag:update', requestData);
|
||||
addToQueue('update_series_spell_tag', {data: requestData});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:spell:tag:update', requestData);
|
||||
success = await tauri.updateSpellTag(requestData.tagId, requestData.name, requestData.color);
|
||||
} else {
|
||||
success = await System.authPutToServer<boolean>('spell/tag/update', requestData, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:spell:tag:update', requestData);
|
||||
addToQueue('update_spell_tag', {data: requestData});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -859,21 +845,21 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
// Series mode - dual logic
|
||||
const deleteData = {tagId, deletedAt};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
success = await window.electron.invoke<boolean>('db:series:spell:tag:delete', deleteData);
|
||||
success = await tauri.deleteSeriesSpellTag(deleteData.tagId, deleteData.deletedAt);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('series/spell/tag/delete', deleteData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:spell:tag:delete', deleteData);
|
||||
addToQueue('delete_series_spell_tag', {data: deleteData});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const requestData = {tagId, bookId: entityId, deletedAt};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
success = await window.electron.invoke<boolean>('db:spell:tag:delete', requestData);
|
||||
success = await tauri.deleteSpellTag(requestData.tagId, requestData.bookId, requestData.deletedAt);
|
||||
} else {
|
||||
success = await System.authDeleteToServer<boolean>('spell/tag/delete', requestData, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:spell:tag:delete', requestData);
|
||||
addToQueue('delete_spell_tag', {data: requestData});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -898,10 +884,7 @@ export function useSpells(config: UseSpellsConfig): UseSpellsReturn {
|
||||
if (selectedSpell?.seriesSpellId) {
|
||||
let seriesSpellResponse: SeriesSpellDetailResponse;
|
||||
if (isCurrentlyOffline() || (isSeriesMode ? localSeries : book?.localBook)) {
|
||||
seriesSpellResponse = await window.electron.invoke<SeriesSpellDetailResponse>(
|
||||
'db:series:spell:detail',
|
||||
{spellId: selectedSpell.seriesSpellId}
|
||||
);
|
||||
seriesSpellResponse = await tauri.getSeriesSpellDetail(selectedSpell.seriesSpellId) as SeriesSpellDetailResponse;
|
||||
} else {
|
||||
seriesSpellResponse = await System.authGetQueryToServer<SeriesSpellDetailResponse>(
|
||||
'series/spell/detail',
|
||||
|
||||
@@ -17,6 +17,7 @@ import System from '@/lib/models/System';
|
||||
import {useTranslations} from 'next-intl';
|
||||
import {SelectBoxProps} from '@/shared/interface';
|
||||
import {ViewMode} from '@/shared/interface';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
const initialWorldState: WorldProps = {
|
||||
id: '',
|
||||
@@ -135,10 +136,7 @@ export function useWorlds(config: UseWorldsConfig): UseWorldsReturn {
|
||||
let response: SeriesWorldProps[];
|
||||
// Dual logic: offline ou livre local → IPC, sinon serveur
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<SeriesWorldProps[]>(
|
||||
'db:series:world:list',
|
||||
{seriesId: bookSeriesId}
|
||||
);
|
||||
response = await tauri.getSeriesWorldList(bookSeriesId);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesWorldProps[]>(
|
||||
'series/world/list',
|
||||
@@ -164,10 +162,7 @@ export function useWorlds(config: UseWorldsConfig): UseWorldsReturn {
|
||||
// Series mode - dual logic
|
||||
let response: SeriesWorldProps[];
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<SeriesWorldProps[]>(
|
||||
'db:series:world:list',
|
||||
{seriesId: entityId}
|
||||
);
|
||||
response = await tauri.getSeriesWorldList(entityId);
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SeriesWorldProps[]>(
|
||||
'series/world/list',
|
||||
@@ -212,9 +207,9 @@ export function useWorlds(config: UseWorldsConfig): UseWorldsReturn {
|
||||
} else {
|
||||
let response: WorldListResponse;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<WorldListResponse>('db:book:worlds:get', {bookid: entityId});
|
||||
response = await tauri.getWorlds(entityId, true) as unknown as WorldListResponse;
|
||||
} else if (book?.localBook) {
|
||||
response = await window.electron.invoke<WorldListResponse>('db:book:worlds:get', {bookid: entityId});
|
||||
response = await tauri.getWorlds(entityId, true) as unknown as WorldListResponse;
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<WorldListResponse>(
|
||||
'book/worlds',
|
||||
@@ -293,11 +288,11 @@ export function useWorlds(config: UseWorldsConfig): UseWorldsReturn {
|
||||
};
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:tool:update', requestData);
|
||||
response = await tauri.updateBookToolSetting(requestData.bookId, requestData.toolName, requestData.enabled);
|
||||
} else {
|
||||
response = await System.authPatchToServer<boolean>('book/tool-setting', requestData, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) {
|
||||
addToQueue('db:book:tool:update', requestData);
|
||||
addToQueue('update_book_tool_setting', {data: requestData});
|
||||
}
|
||||
}
|
||||
if (response && setBook && book) {
|
||||
@@ -333,7 +328,7 @@ export function useWorlds(config: UseWorldsConfig): UseWorldsReturn {
|
||||
name: newWorldName,
|
||||
};
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
newWorldId = await window.electron.invoke<string>('db:series:world:add', addData);
|
||||
newWorldId = await tauri.addSeriesWorld(addData);
|
||||
} else {
|
||||
newWorldId = await System.authPostToServer<string>(
|
||||
'series/world/add',
|
||||
@@ -342,7 +337,7 @@ export function useWorlds(config: UseWorldsConfig): UseWorldsReturn {
|
||||
lang
|
||||
);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:world:add', {...addData, id: newWorldId});
|
||||
addToQueue('add_series_world', {data: {...addData, id: newWorldId}});
|
||||
}
|
||||
}
|
||||
if (!newWorldId) {
|
||||
@@ -355,11 +350,11 @@ export function useWorlds(config: UseWorldsConfig): UseWorldsReturn {
|
||||
bookId: entityId,
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
newWorldId = await window.electron.invoke<string>('db:book:world:add', requestData);
|
||||
newWorldId = await tauri.addWorld(requestData.bookId || entityId, requestData.worldName, requestData.id, requestData.seriesWorldId);
|
||||
} else {
|
||||
newWorldId = await System.authPostToServer<string>('book/world/add', requestData, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:book:world:add', {...requestData, id: newWorldId});
|
||||
addToQueue('add_world', {data: {...requestData, id: newWorldId}});
|
||||
}
|
||||
}
|
||||
if (!newWorldId) {
|
||||
@@ -410,11 +405,11 @@ export function useWorlds(config: UseWorldsConfig): UseWorldsReturn {
|
||||
};
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || localSeries) {
|
||||
response = await window.electron.invoke<boolean>('db:series:world:update', updateData);
|
||||
response = await tauri.updateSeriesWorld(updateData);
|
||||
} else {
|
||||
response = await System.authPatchToServer<boolean>('series/world/update', updateData, userToken, lang);
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === entityId)) {
|
||||
addToQueue('db:series:world:update', updateData);
|
||||
addToQueue('update_series_world', {data: updateData});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
@@ -428,11 +423,11 @@ export function useWorlds(config: UseWorldsConfig): UseWorldsReturn {
|
||||
};
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:world:update', requestData);
|
||||
response = await tauri.updateWorld(requestData.world || requestData);
|
||||
} else {
|
||||
response = await System.authPatchToServer<boolean>('book/world/update', requestData, userToken, lang);
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:book:world:update', requestData);
|
||||
addToQueue('update_world', {data: requestData});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
@@ -469,8 +464,8 @@ export function useWorlds(config: UseWorldsConfig): UseWorldsReturn {
|
||||
|
||||
let seriesWorldId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
seriesWorldId = await window.electron.invoke<string>('db:series:world:add', seriesWorldData);
|
||||
// Mode offline ou livre local → Tauri
|
||||
seriesWorldId = await tauri.addSeriesWorld(seriesWorldData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
seriesWorldId = await System.authPostToServer<string>('series/world/add', {
|
||||
@@ -486,7 +481,7 @@ export function useWorlds(config: UseWorldsConfig): UseWorldsReturn {
|
||||
}, userToken, lang);
|
||||
// Si la série a une copie locale → addToQueue
|
||||
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === bookSeriesId)) {
|
||||
addToQueue('db:series:world:add', {...seriesWorldData, id: seriesWorldId});
|
||||
addToQueue('add_series_world', {data: {...seriesWorldData, id: seriesWorldId}});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,14 +496,14 @@ export function useWorlds(config: UseWorldsConfig): UseWorldsReturn {
|
||||
|
||||
let updateResponse: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
updateResponse = await window.electron.invoke<boolean>('db:book:world:update', updateData);
|
||||
// Mode offline ou livre local → Tauri
|
||||
updateResponse = await tauri.updateWorld(updateData.world || updateData);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
updateResponse = await System.authPostToServer<boolean>('book/world/update', updateData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:book:world:update', updateData);
|
||||
addToQueue('update_world', {data: updateData});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,14 +562,14 @@ export function useWorlds(config: UseWorldsConfig): UseWorldsReturn {
|
||||
|
||||
let worldId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
// Mode offline ou livre local → IPC
|
||||
worldId = await window.electron.invoke<string>('db:book:world:add', requestData);
|
||||
// Mode offline ou livre local → Tauri
|
||||
worldId = await tauri.addWorld(requestData.bookId || entityId, requestData.worldName, requestData.id, requestData.seriesWorldId);
|
||||
} else {
|
||||
// Mode online → Serveur
|
||||
worldId = await System.authPostToServer<string>('book/world/add', requestData, userToken, lang);
|
||||
// Si le livre a une copie locale → addToQueue
|
||||
if (localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === entityId)) {
|
||||
addToQueue('db:book:world:add', {...requestData, id: worldId});
|
||||
addToQueue('add_world', {data: {...requestData, id: worldId}});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {BooksSyncContext} from '@/context/BooksSyncContext';
|
||||
import {CompleteBook} from '@/lib/models/Book';
|
||||
import {BookSyncCompare, SyncedBook} from '@/lib/models/SyncedBook';
|
||||
import {useTranslations} from 'next-intl';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface RemovedItemRecord {
|
||||
removal_id: string;
|
||||
@@ -46,7 +47,7 @@ export default function useSyncBooks() {
|
||||
if (isCurrentlyOffline()) return false;
|
||||
|
||||
try {
|
||||
const bookToSync: CompleteBook = await window.electron.invoke<CompleteBook>('db:book:uploadToServer', bookId);
|
||||
const bookToSync: CompleteBook = await tauri.uploadBookToServer(bookId) as CompleteBook;
|
||||
if (!bookToSync) {
|
||||
errorMessage(t('bookCard.uploadError'));
|
||||
return false;
|
||||
@@ -86,7 +87,7 @@ export default function useSyncBooks() {
|
||||
errorMessage(t('bookCard.downloadError'));
|
||||
return false;
|
||||
}
|
||||
const syncStatus: boolean = await window.electron.invoke<boolean>('db:book:syncSave', response);
|
||||
const syncStatus: boolean = await tauri.syncSaveBook(response);
|
||||
if (!syncStatus) {
|
||||
errorMessage(t('bookCard.downloadError'));
|
||||
return false;
|
||||
@@ -126,7 +127,7 @@ export default function useSyncBooks() {
|
||||
errorMessage(t('bookCard.syncFromServerError'));
|
||||
return false;
|
||||
}
|
||||
const syncStatus: boolean = await window.electron.invoke<boolean>('db:book:sync:toClient', response);
|
||||
const syncStatus: boolean = await tauri.syncBookToClient(response);
|
||||
if (!syncStatus) {
|
||||
errorMessage(t('bookCard.syncFromServerError'));
|
||||
return false;
|
||||
@@ -154,7 +155,7 @@ export default function useSyncBooks() {
|
||||
errorMessage(t('bookCard.syncToServerError'));
|
||||
return false;
|
||||
}
|
||||
const bookToSync: CompleteBook = await window.electron.invoke<CompleteBook>('db:book:sync:toServer', bookToFetch);
|
||||
const bookToSync: CompleteBook = await tauri.syncBookToServer(bookToFetch) as CompleteBook;
|
||||
if (!bookToSync) {
|
||||
errorMessage(t('bookCard.syncToServerError'));
|
||||
return false;
|
||||
@@ -199,19 +200,13 @@ export default function useSyncBooks() {
|
||||
|
||||
if (!isCurrentlyOffline()) {
|
||||
if (offlineMode.isDatabaseInitialized) {
|
||||
localBooksResponse = await window.electron.invoke<SyncedBook[]>('db:books:synced');
|
||||
localBooksResponse = await tauri.getSyncedBooks() as SyncedBook[];
|
||||
|
||||
// Get lastOnlineTimestamp from localStorage (or 0 if not set)
|
||||
const lastOnlineStr: string | null = localStorage.getItem('lastOnlineTimestamp');
|
||||
const lastOnlineTimestamp: number = lastOnlineStr ? parseInt(lastOnlineStr, 10) : 0;
|
||||
|
||||
// Get local tombstones since lastOnlineTimestamp via IPC
|
||||
const localTombstones: RemovedItemRecord[] = await window.electron.invoke<RemovedItemRecord[]>(
|
||||
'db:tombstones:since',
|
||||
lastOnlineTimestamp
|
||||
);
|
||||
const localTombstones: RemovedItemRecord[] = await tauri.getTombstonesSince(lastOnlineTimestamp) as RemovedItemRecord[];
|
||||
|
||||
// Call server with POST and tombstones
|
||||
const serverResponse: SyncedBooksResponse = await System.authPostToServer<SyncedBooksResponse>(
|
||||
'books/synced',
|
||||
{ lastOnlineTimestamp, tombstones: localTombstones },
|
||||
@@ -221,8 +216,7 @@ export default function useSyncBooks() {
|
||||
|
||||
serverBooksResponse = serverResponse.books;
|
||||
|
||||
// Apply server tombstones locally via IPC
|
||||
await window.electron.invoke<void>('db:tombstones:apply:books', serverResponse.tombstones);
|
||||
await tauri.applyBookTombstones(serverResponse.tombstones as tauri.TombstoneRecord[]);
|
||||
} else {
|
||||
// No local DB but online - just get server books without tombstones
|
||||
const serverResponse: SyncedBooksResponse = await System.authPostToServer<SyncedBooksResponse>(
|
||||
@@ -235,7 +229,7 @@ export default function useSyncBooks() {
|
||||
}
|
||||
} else {
|
||||
if (offlineMode.isDatabaseInitialized) {
|
||||
localBooksResponse = await window.electron.invoke<SyncedBook[]>('db:books:synced');
|
||||
localBooksResponse = await tauri.getSyncedBooks() as SyncedBook[];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import OfflineContext from '@/context/OfflineContext';
|
||||
import { SeriesSyncContext } from '@/context/SeriesSyncContext';
|
||||
import { SeriesSyncCompare, SyncedSeries } from '@/lib/models/SyncedSeries';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface RemovedItemRecord {
|
||||
removal_id: string;
|
||||
@@ -72,7 +73,7 @@ export default function useSyncSeries() {
|
||||
if (isCurrentlyOffline()) return false;
|
||||
|
||||
try {
|
||||
const seriesToSync: CompleteSeries = await window.electron.invoke<CompleteSeries>('db:series:uploadToServer', seriesId);
|
||||
const seriesToSync: CompleteSeries = await tauri.uploadSeriesToServer(seriesId) as CompleteSeries;
|
||||
if (!seriesToSync) {
|
||||
errorMessage(t('seriesCard.uploadError'));
|
||||
return false;
|
||||
@@ -130,7 +131,7 @@ export default function useSyncSeries() {
|
||||
return false;
|
||||
}
|
||||
|
||||
const syncStatus: boolean = await window.electron.invoke<boolean>('db:series:syncSave', response);
|
||||
const syncStatus: boolean = await tauri.syncSaveSeries(response);
|
||||
if (!syncStatus) {
|
||||
errorMessage(t('seriesCard.downloadError'));
|
||||
return false;
|
||||
@@ -188,7 +189,7 @@ export default function useSyncSeries() {
|
||||
return false;
|
||||
}
|
||||
|
||||
const syncStatus: boolean = await window.electron.invoke<boolean>('db:series:sync:toClient', response);
|
||||
const syncStatus: boolean = await tauri.syncSeriesToClient(response);
|
||||
if (!syncStatus) {
|
||||
errorMessage(t('seriesCard.syncFromServerError'));
|
||||
return false;
|
||||
@@ -230,10 +231,7 @@ export default function useSyncSeries() {
|
||||
return true;
|
||||
}
|
||||
|
||||
const seriesToSync: CompleteSeries = await window.electron.invoke<CompleteSeries>(
|
||||
'db:series:sync:toServer',
|
||||
seriesToFetch
|
||||
);
|
||||
const seriesToSync: CompleteSeries = await tauri.syncSeriesToServer(seriesToFetch) as CompleteSeries;
|
||||
if (!seriesToSync) {
|
||||
errorMessage(t('seriesCard.syncToServerError'));
|
||||
return false;
|
||||
@@ -288,19 +286,13 @@ export default function useSyncSeries() {
|
||||
|
||||
if (!isCurrentlyOffline()) {
|
||||
if (offlineMode.isDatabaseInitialized) {
|
||||
localSeriesResponse = await window.electron.invoke<SyncedSeries[]>('db:series:synced');
|
||||
localSeriesResponse = await tauri.getSyncedSeries() as SyncedSeries[];
|
||||
|
||||
// Get lastOnlineTimestamp from localStorage (or 0 if not set)
|
||||
const lastOnlineStr: string | null = localStorage.getItem('lastOnlineTimestamp');
|
||||
const lastOnlineTimestamp: number = lastOnlineStr ? parseInt(lastOnlineStr, 10) : 0;
|
||||
|
||||
// Get local tombstones since lastOnlineTimestamp via IPC
|
||||
const localTombstones: RemovedItemRecord[] = await window.electron.invoke<RemovedItemRecord[]>(
|
||||
'db:tombstones:since',
|
||||
lastOnlineTimestamp
|
||||
);
|
||||
const localTombstones: RemovedItemRecord[] = await tauri.getTombstonesSince(lastOnlineTimestamp) as RemovedItemRecord[];
|
||||
|
||||
// Call server with POST and tombstones
|
||||
const serverResponse: SyncedSeriesResponse = await System.authPostToServer<SyncedSeriesResponse>(
|
||||
'series/synced',
|
||||
{ lastOnlineTimestamp, tombstones: localTombstones },
|
||||
@@ -310,8 +302,7 @@ export default function useSyncSeries() {
|
||||
|
||||
serverSeriesResponse = serverResponse.series;
|
||||
|
||||
// Apply server tombstones locally via IPC
|
||||
await window.electron.invoke<void>('db:tombstones:apply:series', serverResponse.tombstones);
|
||||
await tauri.applySeriesTombstones(serverResponse.tombstones as tauri.TombstoneRecord[]);
|
||||
} else {
|
||||
// No local DB but online - just get server series without tombstones
|
||||
const serverResponse: SyncedSeriesResponse = await System.authPostToServer<SyncedSeriesResponse>(
|
||||
@@ -324,7 +315,7 @@ export default function useSyncSeries() {
|
||||
}
|
||||
} else {
|
||||
if (offlineMode.isDatabaseInitialized) {
|
||||
localSeriesResponse = await window.electron.invoke<SyncedSeries[]>('db:series:synced');
|
||||
localSeriesResponse = await tauri.getSyncedSeries() as SyncedSeries[];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface Configs {
|
||||
appVersion: string;
|
||||
}
|
||||
|
||||
const isProduction: boolean = true;
|
||||
const isProduction: boolean = false;
|
||||
|
||||
export const configs: Configs = {
|
||||
apiUrl: isProduction ? 'https://api.eritors.com/' : 'http://localhost:3001/',
|
||||
|
||||
@@ -39,7 +39,7 @@ export default class System{
|
||||
},
|
||||
params: {
|
||||
lang: lang,
|
||||
plateforme: window.electron.platform,
|
||||
plateforme: 'desktop',
|
||||
...params
|
||||
},
|
||||
url: configs.apiUrl + url,
|
||||
@@ -80,7 +80,7 @@ export default class System{
|
||||
},
|
||||
params: {
|
||||
lang: lang,
|
||||
plateforme: window.electron.platform,
|
||||
plateforme: 'desktop',
|
||||
},
|
||||
url: configs.apiUrl + url,
|
||||
data: data
|
||||
@@ -108,7 +108,7 @@ export default class System{
|
||||
},
|
||||
params: {
|
||||
lang: lang,
|
||||
plateforme: window.electron.platform,
|
||||
plateforme: 'desktop',
|
||||
},
|
||||
url: configs.apiUrl + url,
|
||||
data: data
|
||||
@@ -136,7 +136,7 @@ export default class System{
|
||||
url: configs.apiUrl + url,
|
||||
params: {
|
||||
lang: lang,
|
||||
plateforme: window.electron.platform,
|
||||
plateforme: 'desktop',
|
||||
},
|
||||
data: data
|
||||
})
|
||||
@@ -164,7 +164,7 @@ export default class System{
|
||||
url: configs.apiUrl + url,
|
||||
params: {
|
||||
lang: lang,
|
||||
plateforme: window.electron.platform,
|
||||
plateforme: 'desktop',
|
||||
},
|
||||
data: data
|
||||
})
|
||||
@@ -217,7 +217,7 @@ export default class System{
|
||||
const formData: FormData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('lang', lang);
|
||||
formData.append('plateforme', window.electron.platform);
|
||||
formData.append('plateforme', 'desktop');
|
||||
|
||||
const response: AxiosResponse<T> = await axios({
|
||||
method: 'POST',
|
||||
@@ -227,7 +227,7 @@ export default class System{
|
||||
url: configs.apiUrl + url,
|
||||
params: {
|
||||
lang: lang,
|
||||
plateforme: window.electron.platform,
|
||||
plateforme: 'desktop',
|
||||
},
|
||||
data: formData,
|
||||
});
|
||||
@@ -255,7 +255,7 @@ export default class System{
|
||||
url: configs.apiUrl + url,
|
||||
params: {
|
||||
lang: lang,
|
||||
plateforme: window.electron.platform,
|
||||
plateforme: 'desktop',
|
||||
},
|
||||
data: data
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Database Error Handler for Frontend
|
||||
* Handles errors from Electron IPC calls
|
||||
* Handles errors from Tauri invoke calls
|
||||
*/
|
||||
|
||||
export interface SerializedError {
|
||||
@@ -69,7 +69,7 @@ export async function handleDbOperation<T>(
|
||||
* const { data, error, loading, execute } = useDbOperation();
|
||||
*
|
||||
* const loadBooks = async () => {
|
||||
* await execute(() => window.electron.invoke('db:book:getAll'));
|
||||
* await execute(() => tauri.getBooks());
|
||||
* };
|
||||
*/
|
||||
export function useDbOperation<T>() {
|
||||
|
||||
243
package-lock.json
generated
243
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "eritorsscribe",
|
||||
"version": "0.3.1",
|
||||
"version": "0.4.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "eritorsscribe",
|
||||
"version": "0.3.1",
|
||||
"version": "0.4.1",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||
@@ -15,6 +15,8 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@fortawesome/react-fontawesome": "^3.1.0",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||
"@tiptap/extension-color": "^3.10.7",
|
||||
"@tiptap/extension-gapcursor": "^3.10.7",
|
||||
"@tiptap/extension-highlight": "^3.10.7",
|
||||
@@ -40,6 +42,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron/notarize": "^3.1.1",
|
||||
"@tauri-apps/cli": "^2.10.1",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/pdfkit": "^0.17.5",
|
||||
@@ -2399,6 +2402,242 @@
|
||||
"tailwindcss": "4.1.18"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz",
|
||||
"integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz",
|
||||
"integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
"tauri": "tauri.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.10.1",
|
||||
"@tauri-apps/cli-darwin-x64": "2.10.1",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.10.1",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.10.1",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.10.1",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.10.1",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.10.1",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.10.1",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.10.1",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz",
|
||||
"integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz",
|
||||
"integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz",
|
||||
"integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz",
|
||||
"integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz",
|
||||
"integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz",
|
||||
"integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz",
|
||||
"integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz",
|
||||
"integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz",
|
||||
"integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz",
|
||||
"integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz",
|
||||
"integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-shell": {
|
||||
"version": "2.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz",
|
||||
"integrity": "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tiptap/core": {
|
||||
"version": "3.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.19.0.tgz",
|
||||
|
||||
@@ -9,7 +9,11 @@
|
||||
"build:mac": "next build && tsc --project tsconfig.electron.json && tsc --project tsconfig.preload.json && electron-builder build --mac",
|
||||
"build:win": "next build && tsc --project tsconfig.electron.json && tsc --project tsconfig.preload.json && electron-builder build --win",
|
||||
"build:linux": "next build && tsc --project tsconfig.electron.json && tsc --project tsconfig.preload.json && electron-builder build --linux",
|
||||
"build:all": "next build && tsc --project tsconfig.electron.json && tsc --project tsconfig.preload.json && electron-builder build --mac --win --linux"
|
||||
"build:all": "next build && tsc --project tsconfig.electron.json && tsc --project tsconfig.preload.json && electron-builder build --mac --win --linux",
|
||||
"next:dev": "next dev -p 4000",
|
||||
"next:build": "next build",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "tauri build"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -17,6 +21,7 @@
|
||||
"description": "",
|
||||
"devDependencies": {
|
||||
"@electron/notarize": "^3.1.1",
|
||||
"@tauri-apps/cli": "^2.10.1",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/pdfkit": "^0.17.5",
|
||||
@@ -36,6 +41,8 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
"@fortawesome/react-fontawesome": "^3.1.0",
|
||||
"@tailwindcss/postcss": "^4.1.17",
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||
"@tiptap/extension-color": "^3.10.7",
|
||||
"@tiptap/extension-gapcursor": "^3.10.7",
|
||||
"@tiptap/extension-highlight": "^3.10.7",
|
||||
|
||||
2
src-tauri/src/domains/act/mod.rs
Normal file
2
src-tauri/src/domains/act/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod repo;
|
||||
pub mod service;
|
||||
219
src-tauri/src/domains/act/repo.rs
Normal file
219
src-tauri/src/domains/act/repo.rs
Normal file
@@ -0,0 +1,219 @@
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
pub struct BookActSummariesTable {
|
||||
pub act_sum_id: String,
|
||||
pub book_id: String,
|
||||
pub user_id: String,
|
||||
pub act_index: i64,
|
||||
pub last_update: i64,
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
pub struct SyncedActSummaryResult {
|
||||
pub act_sum_id: String,
|
||||
pub book_id: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct ActQuery {
|
||||
pub act_index: i64,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
/// Fetches all acts for a specific book and user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of ActQuery objects containing act index and summary.
|
||||
/// Errors if the database operation fails.
|
||||
pub fn fetch_all_acts(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<ActQuery>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT act_index, summary FROM book_act_summaries WHERE book_id=?1 AND user_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les actes.".to_string() } else { "Unable to retrieve acts.".to_string() }))?;
|
||||
|
||||
let acts = statement
|
||||
.query_map(params![book_id, user_id], |query_row| {
|
||||
Ok(ActQuery { act_index: query_row.get(0)?, summary: query_row.get(1)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les actes.".to_string() } else { "Unable to retrieve acts.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les actes.".to_string() } else { "Unable to retrieve acts.".to_string() }))?;
|
||||
|
||||
Ok(acts)
|
||||
}
|
||||
|
||||
/// Updates the summary of an existing act.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `act_id` - The unique identifier of the act summary
|
||||
/// * `summary` - The new summary text
|
||||
/// * `last_update` - The timestamp of the last update in seconds
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the update was successful, false otherwise.
|
||||
/// Errors if the database operation fails.
|
||||
pub fn update_act_summary(
|
||||
conn: &Connection, user_id: &str, book_id: &str, act_id: i64, summary: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute(
|
||||
"UPDATE book_act_summaries SET summary=?1, last_update=?2 WHERE user_id=?3 AND book_id=?4 AND act_sum_id=?5",
|
||||
params![summary, last_update, user_id, book_id, act_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le résumé de l'acte.".to_string() } else { "Unable to update act summary.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Inserts a new act summary into the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `act_summary_id` - The unique identifier for the new act summary
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `act_id` - The act index number
|
||||
/// * `act_summary` - The summary text for the act
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the act summary ID if insertion was successful.
|
||||
/// Errors if the database operation fails.
|
||||
pub fn insert_act_summary(
|
||||
conn: &Connection, act_summary_id: &str, user_id: &str, book_id: &str, act_id: i64,
|
||||
act_summary: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<String> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO book_act_summaries (act_sum_id, book_id, user_id, act_index, summary, last_update) VALUES (?1,?2,?3,?4,?5,?6)",
|
||||
params![act_summary_id, book_id, user_id, act_id, act_summary, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le résumé de l'acte.".to_string() } else { "Unable to add act summary.".to_string() }))?;
|
||||
|
||||
if insert_result > 0 {
|
||||
Ok(act_summary_id.to_string())
|
||||
} else {
|
||||
Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout du résumé de l'acte.".to_string() } else { "Error adding act summary.".to_string() }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches all act summaries for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of BookActSummariesTable objects.
|
||||
/// Errors if the database operation fails.
|
||||
pub fn fetch_book_act_summaries(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookActSummariesTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT act_sum_id, book_id, user_id, act_index, summary, last_update FROM book_act_summaries WHERE user_id=?1 AND book_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les résumés des actes.".to_string() } else { "Unable to retrieve act summaries.".to_string() }))?;
|
||||
|
||||
let summaries = statement
|
||||
.query_map(params![user_id, book_id], |query_row| {
|
||||
Ok(BookActSummariesTable {
|
||||
act_sum_id: query_row.get(0)?, book_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, act_index: query_row.get(3)?,
|
||||
summary: query_row.get(4)?, last_update: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les résumés des actes.".to_string() } else { "Unable to retrieve act summaries.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les résumés des actes.".to_string() } else { "Unable to retrieve act summaries.".to_string() }))?;
|
||||
|
||||
Ok(summaries)
|
||||
}
|
||||
|
||||
/// Fetches all synced act summaries for a user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of SyncedActSummaryResult objects containing sync metadata.
|
||||
/// Errors if the database operation fails.
|
||||
pub fn fetch_synced_act_summaries(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedActSummaryResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT act_sum_id, book_id, last_update FROM book_act_summaries WHERE user_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les résumés d'actes synchronisés.".to_string() } else { "Unable to retrieve synced act summaries.".to_string() }))?;
|
||||
|
||||
let synced_act_summaries = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedActSummaryResult { act_sum_id: query_row.get(0)?, book_id: query_row.get(1)?, last_update: query_row.get(2)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les résumés d'actes synchronisés.".to_string() } else { "Unable to retrieve synced act summaries.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les résumés d'actes synchronisés.".to_string() } else { "Unable to retrieve synced act summaries.".to_string() }))?;
|
||||
|
||||
Ok(synced_act_summaries)
|
||||
}
|
||||
|
||||
/// Inserts a synced act summary from remote data.
|
||||
/// * `conn` - Database connection
|
||||
/// * `act_sum_id` - The unique identifier of the act summary
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `act_index` - The act index number
|
||||
/// * `summary` - The summary text (can be null)
|
||||
/// * `last_update` - The timestamp of the last update in seconds
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the insertion was successful, false otherwise.
|
||||
/// Errors if the database operation fails.
|
||||
pub fn insert_sync_act_summary(
|
||||
conn: &Connection, act_sum_id: &str, book_id: &str, user_id: &str, act_index: i64,
|
||||
summary: Option<&str>, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO book_act_summaries (act_sum_id, book_id, user_id, act_index, summary, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![act_sum_id, book_id, user_id, act_index, summary, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le résumé d'acte.".to_string() } else { "Unable to insert act summary.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Fetches a complete act summary by its unique identifier.
|
||||
/// * `conn` - Database connection
|
||||
/// * `id` - The unique identifier of the act summary
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of BookActSummariesTable objects.
|
||||
/// Errors if the database operation fails.
|
||||
pub fn fetch_complete_act_summary_by_id(conn: &Connection, id: &str, lang: Lang) -> AppResult<Vec<BookActSummariesTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT act_sum_id, book_id, user_id, act_index, summary, last_update FROM book_act_summaries WHERE act_sum_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le résumé d'acte complet.".to_string() } else { "Unable to retrieve complete act summary.".to_string() }))?;
|
||||
|
||||
let act_summary = statement
|
||||
.query_map(params![id], |query_row| {
|
||||
Ok(BookActSummariesTable {
|
||||
act_sum_id: query_row.get(0)?, book_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, act_index: query_row.get(3)?,
|
||||
summary: query_row.get(4)?, last_update: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le résumé d'acte complet.".to_string() } else { "Unable to retrieve complete act summary.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le résumé d'acte complet.".to_string() } else { "Unable to retrieve complete act summary.".to_string() }))?;
|
||||
|
||||
Ok(act_summary)
|
||||
}
|
||||
|
||||
/// Checks if an act summary exists for a given user, book, and act index.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `act_index` - The act index number to check
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the act summary exists, false otherwise.
|
||||
/// Errors if the database operation fails.
|
||||
pub fn act_summarize_exist(conn: &Connection, user_id: &str, book_id: &str, act_index: i64, lang: Lang) -> AppResult<bool> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT 1 FROM book_act_summaries WHERE user_id =?1 AND book_id =?2 AND act_index = ?3")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du résumé de l'acte.".to_string() } else { "Unable to check act summary existence.".to_string() }))?;
|
||||
|
||||
let existence_check = statement
|
||||
.query_row(params![user_id, book_id, act_index], |_query_row| Ok(true))
|
||||
.optional()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du résumé de l'acte.".to_string() } else { "Unable to check act summary existence.".to_string() }))?;
|
||||
|
||||
Ok(existence_check.is_some())
|
||||
}
|
||||
439
src-tauri/src/domains/act/service.rs
Normal file
439
src-tauri/src/domains/act/service.rs
Normal file
@@ -0,0 +1,439 @@
|
||||
use rusqlite::Connection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::crypto::encryption::{encrypt_data_with_user_key, decrypt_data_with_user_key, hash_element};
|
||||
use crate::crypto::key_manager::get_user_encryption_key;
|
||||
use crate::domains::act::repo;
|
||||
use crate::domains::chapter::repo as chapter_repo;
|
||||
use crate::domains::chapter::service as chapter_service;
|
||||
use crate::domains::incident::repo as incident_repo;
|
||||
use crate::domains::plotpoint::repo as plotpoint_repo;
|
||||
use crate::error::AppResult;
|
||||
use crate::helpers::{create_unique_id, timestamp_in_seconds};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ActProps {
|
||||
pub id: i64,
|
||||
pub summary: Option<String>,
|
||||
pub incidents: Option<Vec<IncidentProps>>,
|
||||
pub plot_points: Option<Vec<PlotPointProps>>,
|
||||
pub chapters: Option<Vec<ActChapter>>,
|
||||
}
|
||||
|
||||
pub struct ActStory {
|
||||
pub act_id: i64,
|
||||
pub summary: String,
|
||||
pub chapter_summary: String,
|
||||
pub chapter_goal: String,
|
||||
pub incidents: Vec<IncidentStory>,
|
||||
pub plot_points: Vec<PlotPointStory>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ActChapter {
|
||||
pub chapter_info_id: i64,
|
||||
pub chapter_id: String,
|
||||
pub title: String,
|
||||
pub chapter_order: i64,
|
||||
pub act_id: i64,
|
||||
pub incident_id: Option<String>,
|
||||
pub plot_point_id: Option<String>,
|
||||
pub summary: String,
|
||||
pub goal: String,
|
||||
}
|
||||
|
||||
pub struct SyncedActSummary {
|
||||
pub id: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IncidentProps {
|
||||
pub incident_id: String,
|
||||
pub title: String,
|
||||
pub summary: String,
|
||||
pub chapters: Option<Vec<ActChapter>>,
|
||||
}
|
||||
|
||||
pub struct IncidentStory {
|
||||
pub incident_title: String,
|
||||
pub incident_summary: String,
|
||||
pub chapter_summary: String,
|
||||
pub chapter_goal: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlotPointProps {
|
||||
pub plot_point_id: String,
|
||||
pub title: String,
|
||||
pub summary: String,
|
||||
pub linked_incident_id: Option<String>,
|
||||
pub chapters: Option<Vec<ActChapter>>,
|
||||
}
|
||||
|
||||
pub struct PlotPointStory {
|
||||
pub plot_title: String,
|
||||
pub plot_summary: String,
|
||||
pub chapter_summary: String,
|
||||
pub chapter_goal: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChapterProps {
|
||||
pub chapter_id: String,
|
||||
pub title: String,
|
||||
pub chapter_order: i64,
|
||||
}
|
||||
|
||||
/// Retrieves all chapters linked to acts for a specific book.
|
||||
/// Decrypts titles, summaries, and goals using the user's encryption key.
|
||||
/// Uses a cache to avoid decrypting the same chapter title multiple times.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns a list of decrypted act chapters.
|
||||
fn get_all_chapter_from_acts(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<ActChapter>> {
|
||||
let act_chapter_query_results: Vec<chapter_repo::ActChapterQuery> = chapter_repo::fetch_all_chapter_for_acts(conn, user_id, book_id, lang)?;
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
if act_chapter_query_results.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut act_chapters: Vec<ActChapter> = Vec::new();
|
||||
let mut decrypted_title_cache: Vec<(String, String)> = Vec::new();
|
||||
|
||||
for chapter_query_result in &act_chapter_query_results {
|
||||
let decrypted_title: String = if let Some(cached) = decrypted_title_cache.iter().find(|(id, _)| id == &chapter_query_result.chapter_id) {
|
||||
cached.1.clone()
|
||||
} else {
|
||||
let title: String = decrypt_data_with_user_key(&chapter_query_result.title, &user_encryption_key)?;
|
||||
decrypted_title_cache.push((chapter_query_result.chapter_id.clone(), title.clone()));
|
||||
title
|
||||
};
|
||||
|
||||
let decrypted_goal: String = if chapter_query_result.goal.is_empty() { String::new() } else { decrypt_data_with_user_key(&chapter_query_result.goal, &user_encryption_key)? };
|
||||
let decrypted_summary: String = if chapter_query_result.summary.is_empty() { String::new() } else { decrypt_data_with_user_key(&chapter_query_result.summary, &user_encryption_key)? };
|
||||
|
||||
act_chapters.push(ActChapter {
|
||||
chapter_info_id: chapter_query_result.chapter_info_id,
|
||||
chapter_id: chapter_query_result.chapter_id.clone(),
|
||||
title: decrypted_title,
|
||||
chapter_order: chapter_query_result.chapter_order,
|
||||
act_id: chapter_query_result.act_id,
|
||||
incident_id: chapter_query_result.incident_id.clone(),
|
||||
plot_point_id: chapter_query_result.plot_point_id.clone(),
|
||||
summary: decrypted_summary,
|
||||
goal: decrypted_goal,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(act_chapters)
|
||||
}
|
||||
|
||||
/// Retrieves all incidents for a specific book with their associated chapters.
|
||||
/// Decrypts incident titles and summaries using the user's encryption key.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `act_chapters` - Array of chapters from acts to associate with incidents
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns a list of incident properties with decrypted data.
|
||||
fn get_incidents(conn: &Connection, user_id: &str, book_id: &str, act_chapters: &[ActChapter], lang: Lang) -> AppResult<Vec<IncidentProps>> {
|
||||
let incident_query_results: Vec<incident_repo::IncidentQuery> = incident_repo::fetch_all_incitent_incidents(conn, user_id, book_id, lang)?;
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let mut incidents: Vec<IncidentProps> = Vec::new();
|
||||
|
||||
if incident_query_results.is_empty() {
|
||||
return Ok(incidents);
|
||||
}
|
||||
|
||||
for incident_record in &incident_query_results {
|
||||
let mut associated_chapters: Vec<ActChapter> = Vec::new();
|
||||
for chapter in act_chapters {
|
||||
if chapter.incident_id.as_deref() == Some(&incident_record.incident_id) {
|
||||
associated_chapters.push(ActChapter {
|
||||
chapter_info_id: chapter.chapter_info_id,
|
||||
chapter_id: chapter.chapter_id.clone(),
|
||||
title: chapter.title.clone(),
|
||||
chapter_order: chapter.chapter_order,
|
||||
act_id: chapter.act_id,
|
||||
incident_id: chapter.incident_id.clone(),
|
||||
plot_point_id: chapter.plot_point_id.clone(),
|
||||
summary: chapter.summary.clone(),
|
||||
goal: chapter.goal.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
incidents.push(IncidentProps {
|
||||
incident_id: incident_record.incident_id.clone(),
|
||||
title: if incident_record.title.is_empty() { String::new() } else { decrypt_data_with_user_key(&incident_record.title, &user_key)? },
|
||||
summary: if incident_record.summary.is_empty() { String::new() } else { decrypt_data_with_user_key(&incident_record.summary, &user_key)? },
|
||||
chapters: Some(associated_chapters),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(incidents)
|
||||
}
|
||||
|
||||
/// Retrieves all plot points for a specific book with their associated chapters.
|
||||
/// Decrypts plot point titles and summaries using the user's encryption key.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `act_chapters` - Array of act chapters to associate with plot points
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns a list of plot point properties with their associated chapters.
|
||||
fn get_plot_points(conn: &Connection, user_id: &str, book_id: &str, act_chapters: &[ActChapter], lang: Lang) -> AppResult<Vec<PlotPointProps>> {
|
||||
let plot_point_query_results: Vec<plotpoint_repo::PlotPointQuery> = plotpoint_repo::fetch_all_plot_points(conn, user_id, book_id, lang)?;
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
let mut plot_points: Vec<PlotPointProps> = Vec::new();
|
||||
|
||||
if plot_point_query_results.is_empty() {
|
||||
return Ok(plot_points);
|
||||
}
|
||||
|
||||
for plot_point_row in &plot_point_query_results {
|
||||
let mut associated_chapters: Vec<ActChapter> = Vec::new();
|
||||
for chapter in act_chapters {
|
||||
if chapter.plot_point_id.as_deref() == Some(&plot_point_row.plot_point_id) {
|
||||
associated_chapters.push(ActChapter {
|
||||
chapter_info_id: chapter.chapter_info_id,
|
||||
chapter_id: chapter.chapter_id.clone(),
|
||||
title: chapter.title.clone(),
|
||||
chapter_order: chapter.chapter_order,
|
||||
act_id: chapter.act_id,
|
||||
incident_id: chapter.incident_id.clone(),
|
||||
plot_point_id: chapter.plot_point_id.clone(),
|
||||
summary: chapter.summary.clone(),
|
||||
goal: chapter.goal.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
plot_points.push(PlotPointProps {
|
||||
plot_point_id: plot_point_row.plot_point_id.clone(),
|
||||
title: if plot_point_row.title.is_empty() { String::new() } else { decrypt_data_with_user_key(&plot_point_row.title, &user_encryption_key)? },
|
||||
summary: if plot_point_row.summary.is_empty() { String::new() } else { decrypt_data_with_user_key(&plot_point_row.summary, &user_encryption_key)? },
|
||||
linked_incident_id: plot_point_row.linked_incident_id.clone(),
|
||||
chapters: Some(associated_chapters),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(plot_points)
|
||||
}
|
||||
|
||||
/// Updates chapter information for multiple chapters including summary and goal.
|
||||
/// Encrypts summaries and goals before storing in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `chapters` - Array of ActChapter objects containing updated information
|
||||
|
||||
/// Updates a chapter's title and order.
|
||||
/// Encrypts the title and generates a hash before storing.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `chapter_id` - The unique identifier of the chapter
|
||||
/// * `title` - The plain text title
|
||||
/// * `chapter_order` - The chapter order position
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the update was successful.
|
||||
fn update_chapter(conn: &Connection, user_id: &str, chapter_id: &str, title: &str, chapter_order: i64, lang: Lang) -> AppResult<bool> {
|
||||
let hashed_title: String = hash_element(title);
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
let encrypted_title: String = encrypt_data_with_user_key(title, &user_encryption_key)?;
|
||||
chapter_repo::update_chapter(conn, user_id, chapter_id, &encrypted_title, &hashed_title, chapter_order, timestamp_in_seconds(), lang)
|
||||
}
|
||||
|
||||
/// Retrieves all acts data for a specific book, including chapters, incidents, and plot points.
|
||||
/// Decrypts summaries using the user's encryption key.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns a list of Act objects with their associated data.
|
||||
pub fn get_acts_data(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<ActProps>> {
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
let act_chapters: Vec<ActChapter> = get_all_chapter_from_acts(conn, user_id, book_id, lang)?;
|
||||
let act_queries: Vec<repo::ActQuery> = repo::fetch_all_acts(conn, user_id, book_id, lang)?;
|
||||
let book_incidents: Vec<IncidentProps> = get_incidents(conn, user_id, book_id, &act_chapters, lang)?;
|
||||
let book_plot_points: Vec<PlotPointProps> = get_plot_points(conn, user_id, book_id, &act_chapters, lang)?;
|
||||
|
||||
let mut acts: Vec<ActProps> = Vec::new();
|
||||
|
||||
acts.push(ActProps {
|
||||
id: 1,
|
||||
summary: Some(String::new()),
|
||||
incidents: None,
|
||||
plot_points: None,
|
||||
chapters: Some(act_chapters.iter().filter(|chapter| chapter.act_id == 1).map(|chapter| ActChapter {
|
||||
chapter_info_id: chapter.chapter_info_id,
|
||||
chapter_id: chapter.chapter_id.clone(),
|
||||
title: chapter.title.clone(),
|
||||
chapter_order: chapter.chapter_order,
|
||||
act_id: chapter.act_id,
|
||||
incident_id: chapter.incident_id.clone(),
|
||||
plot_point_id: chapter.plot_point_id.clone(),
|
||||
summary: chapter.summary.clone(),
|
||||
goal: chapter.goal.clone(),
|
||||
}).collect()),
|
||||
});
|
||||
|
||||
acts.push(ActProps {
|
||||
id: 2,
|
||||
summary: Some(String::new()),
|
||||
incidents: Some(book_incidents),
|
||||
plot_points: None,
|
||||
chapters: None,
|
||||
});
|
||||
|
||||
acts.push(ActProps {
|
||||
id: 3,
|
||||
summary: Some(String::new()),
|
||||
incidents: None,
|
||||
plot_points: Some(book_plot_points),
|
||||
chapters: None,
|
||||
});
|
||||
|
||||
acts.push(ActProps {
|
||||
id: 4,
|
||||
summary: Some(String::new()),
|
||||
incidents: None,
|
||||
plot_points: None,
|
||||
chapters: Some(act_chapters.iter().filter(|chapter| chapter.act_id == 4).map(|chapter| ActChapter {
|
||||
chapter_info_id: chapter.chapter_info_id,
|
||||
chapter_id: chapter.chapter_id.clone(),
|
||||
title: chapter.title.clone(),
|
||||
chapter_order: chapter.chapter_order,
|
||||
act_id: chapter.act_id,
|
||||
incident_id: chapter.incident_id.clone(),
|
||||
plot_point_id: chapter.plot_point_id.clone(),
|
||||
summary: chapter.summary.clone(),
|
||||
goal: chapter.goal.clone(),
|
||||
}).collect()),
|
||||
});
|
||||
|
||||
acts.push(ActProps {
|
||||
id: 5,
|
||||
summary: Some(String::new()),
|
||||
incidents: None,
|
||||
plot_points: None,
|
||||
chapters: Some(act_chapters.iter().filter(|chapter| chapter.act_id == 5).map(|chapter| ActChapter {
|
||||
chapter_info_id: chapter.chapter_info_id,
|
||||
chapter_id: chapter.chapter_id.clone(),
|
||||
title: chapter.title.clone(),
|
||||
chapter_order: chapter.chapter_order,
|
||||
act_id: chapter.act_id,
|
||||
incident_id: chapter.incident_id.clone(),
|
||||
plot_point_id: chapter.plot_point_id.clone(),
|
||||
summary: chapter.summary.clone(),
|
||||
goal: chapter.goal.clone(),
|
||||
}).collect()),
|
||||
});
|
||||
|
||||
if !act_queries.is_empty() {
|
||||
for act_query in &act_queries {
|
||||
let act_index: usize = (act_query.act_index - 1) as usize;
|
||||
if act_index < acts.len() {
|
||||
acts[act_index].summary = if !act_query.summary.is_empty() {
|
||||
Some(decrypt_data_with_user_key(&act_query.summary, &user_encryption_key)?)
|
||||
} else {
|
||||
Some(String::new())
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(acts)
|
||||
}
|
||||
|
||||
/// Updates multiple acts including their summaries, incidents, plot points, and chapter information.
|
||||
/// Encrypts all sensitive data before storing in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `acts` - Array of act properties to update
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `user_key` - The user's encryption key for data encryption
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true when all updates are complete.
|
||||
pub fn update_act(
|
||||
conn: &Connection, acts: &[ActProps], user_id: &str, book_id: &str, user_key: &str, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
for act in acts {
|
||||
let act_incidents: &[IncidentProps] = act.incidents.as_deref().unwrap_or(&[]);
|
||||
let act_id: i64 = act.id;
|
||||
|
||||
if act_id == 1 || act_id == 4 || act_id == 5 {
|
||||
let encrypted_act_summary: String = if let Some(ref summary) = act.summary {
|
||||
if summary.is_empty() { String::new() } else { encrypt_data_with_user_key(summary, user_key)? }
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let update_result = repo::update_act_summary(conn, user_id, book_id, act_id, &encrypted_act_summary, timestamp_in_seconds(), lang);
|
||||
if update_result.is_err() {
|
||||
let new_act_summary_id: String = create_unique_id(None);
|
||||
repo::insert_act_summary(conn, &new_act_summary_id, user_id, book_id, act_id, &encrypted_act_summary, timestamp_in_seconds(), lang)?;
|
||||
}
|
||||
if let Some(ref chapters) = act.chapters {
|
||||
chapter_service::update_chapter_infos(conn, chapters, user_id, act_id, book_id, None, None, lang)?;
|
||||
}
|
||||
} else if act_id == 2 {
|
||||
for incident in act_incidents {
|
||||
let encrypted_incident_summary: String = if incident.summary.is_empty() { String::new() } else { encrypt_data_with_user_key(&incident.summary, user_key)? };
|
||||
let incident_id: &str = &incident.incident_id;
|
||||
let incident_title: &str = &incident.title;
|
||||
let hashed_incident_title: String = hash_element(incident_title);
|
||||
let encrypted_incident_title: String = encrypt_data_with_user_key(incident_title, user_key)?;
|
||||
incident_repo::update_incident(conn, user_id, book_id, incident_id, &encrypted_incident_title, &hashed_incident_title, &encrypted_incident_summary, timestamp_in_seconds(), lang)?;
|
||||
if let Some(ref chapters) = incident.chapters {
|
||||
chapter_service::update_chapter_infos(conn, chapters, user_id, act_id, book_id, Some(incident_id), None, lang)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let act_plot_points: &[PlotPointProps] = act.plot_points.as_deref().unwrap_or(&[]);
|
||||
for plot_point in act_plot_points {
|
||||
let encrypted_plot_point_summary: String = if plot_point.summary.is_empty() { String::new() } else { encrypt_data_with_user_key(&plot_point.summary, user_key)? };
|
||||
let plot_point_id: &str = &plot_point.plot_point_id;
|
||||
let plot_point_title: &str = &plot_point.title;
|
||||
let hashed_plot_point_title: String = hash_element(plot_point_title);
|
||||
let encrypted_plot_point_title: String = encrypt_data_with_user_key(plot_point_title, user_key)?;
|
||||
plotpoint_repo::update_plot_point(conn, user_id, book_id, plot_point_id, &encrypted_plot_point_title, &hashed_plot_point_title, &encrypted_plot_point_summary, timestamp_in_seconds(), lang)?;
|
||||
if let Some(ref chapters) = plot_point.chapters {
|
||||
chapter_service::update_chapter_infos(conn, chapters, user_id, act_id, book_id, None, Some(plot_point_id), lang)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Updates the story structure including acts and main chapters.
|
||||
/// Encrypts chapter titles and updates their order in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `acts` - Array of act properties to update
|
||||
/// * `main_chapters` - Array of main chapter properties to update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true when all updates are complete.
|
||||
pub fn update_story(
|
||||
conn: &Connection, user_id: &str, book_id: &str, acts: &[ActProps], main_chapters: &[ChapterProps], lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
update_act(conn, acts, user_id, book_id, &user_encryption_key, lang)?;
|
||||
|
||||
for chapter in main_chapters {
|
||||
let chapter_id: &str = &chapter.chapter_id;
|
||||
let chapter_title: &str = &chapter.title;
|
||||
let chapter_order: i64 = chapter.chapter_order;
|
||||
update_chapter(conn, user_id, chapter_id, chapter_title, chapter_order, lang)?;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
494
src-tauri/src/domains/book/commands.rs
Normal file
494
src-tauri/src/domains/book/commands.rs
Normal file
@@ -0,0 +1,494 @@
|
||||
use serde::Deserialize;
|
||||
use tauri::State;
|
||||
|
||||
use crate::db::connection::DbManager;
|
||||
use crate::domains::act::service as act_service;
|
||||
use crate::domains::book::service;
|
||||
use crate::domains::chapter::service as chapter_service;
|
||||
use crate::domains::download::service as download_service;
|
||||
use crate::domains::export::service as export_service;
|
||||
use crate::domains::guideline::service as guideline_service;
|
||||
use crate::domains::incident::service as incident_service;
|
||||
use crate::domains::issue::service as issue_service;
|
||||
use crate::domains::plotpoint::service as plotpoint_service;
|
||||
use crate::domains::sync::service as sync_service;
|
||||
use crate::domains::upload::service as upload_service;
|
||||
use crate::domains::world::service as world_service;
|
||||
use crate::error::AppError;
|
||||
use crate::shared::session::SessionState;
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────
|
||||
|
||||
fn get_session(session: &State<SessionState>) -> Result<(String, crate::shared::types::Lang), AppError> {
|
||||
let session_guard = session.lock().map_err(|e| AppError::Internal(format!("Session lock failed: {}", e)))?;
|
||||
let user_id = session_guard.get_user_id().map_err(|e| AppError::Auth(e))?.to_string();
|
||||
let lang = session_guard.lang;
|
||||
Ok((user_id, lang))
|
||||
}
|
||||
|
||||
fn get_conn<'a>(db: &'a State<DbManager>, _user_id: &str) -> Result<std::sync::MutexGuard<'a, crate::db::connection::DatabaseManager>, AppError> {
|
||||
db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))
|
||||
}
|
||||
|
||||
// ─── Book CRUD ────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateBookData {
|
||||
pub title: String,
|
||||
pub sub_title: Option<String>,
|
||||
pub summary: Option<String>,
|
||||
pub book_type: String,
|
||||
pub serie_id: Option<i64>,
|
||||
pub desired_release_date: Option<String>,
|
||||
pub desired_word_count: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateBookBasicInfoData {
|
||||
pub book_id: String,
|
||||
pub title: String,
|
||||
pub sub_title: String,
|
||||
pub summary: String,
|
||||
pub publication_date: String,
|
||||
pub word_count: i64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteBookData {
|
||||
pub id: String,
|
||||
pub deleted_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateBookToolData {
|
||||
pub book_id: String,
|
||||
pub tool_name: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_books(db: State<DbManager>, session: State<SessionState>) -> Result<Vec<service::BookProps>, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_books(conn, &user_id, lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_book(book_id: String, db: State<DbManager>, session: State<SessionState>) -> Result<service::BookProps, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_book(conn, &user_id, &book_id, lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_book_basic_information(book_id: String, db: State<DbManager>, session: State<SessionState>) -> Result<service::BookProps, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_book(conn, &user_id, &book_id, lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_book(data: CreateBookData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::add_book(conn, None, &user_id, &data.title, data.sub_title.as_deref().unwrap_or(""), data.summary.as_deref().unwrap_or(""), &data.book_type, data.serie_id.unwrap_or(0), data.desired_release_date.as_deref(), data.desired_word_count.unwrap_or(0), lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_book_basic_info(data: UpdateBookBasicInfoData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::update_book_basic_information(conn, &user_id, &data.title, &data.sub_title, &data.summary, Some(&data.publication_date), data.word_count, &data.book_id, lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_book(data: DeleteBookData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::remove_book(conn, &user_id, &data.id, data.deleted_at, lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_book_tool_setting(data: UpdateBookToolData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::update_book_tool_setting(conn, &user_id, &data.book_id, &data.tool_name, data.enabled, lang)
|
||||
}
|
||||
|
||||
// ─── Story ────────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetStoryData {
|
||||
pub book_id: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_book_story(data: GetStoryData, db: State<DbManager>, session: State<SessionState>) -> Result<serde_json::Value, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
let acts = act_service::get_acts_data(conn, &user_id, &data.book_id, lang)?;
|
||||
let issues = issue_service::get_issues_from_book(conn, &user_id, &data.book_id, lang)?;
|
||||
let main_chapters = chapter_service::get_all_chapters_from_a_book(conn, &user_id, &data.book_id, lang)?;
|
||||
Ok(serde_json::json!({ "acts": acts, "issues": issues, "mainChapter": main_chapters }))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateStoryData {
|
||||
pub book_id: String,
|
||||
pub acts: Vec<act_service::ActProps>,
|
||||
pub main_chapters: Vec<act_service::ChapterProps>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_book_story(data: UpdateStoryData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
act_service::update_story(conn, &user_id, &data.book_id, &data.acts, &data.main_chapters, lang)
|
||||
}
|
||||
|
||||
// ─── Incidents ────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddIncidentData {
|
||||
pub book_id: String,
|
||||
pub name: String,
|
||||
pub incident_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveIncidentData {
|
||||
pub book_id: String,
|
||||
pub incident_id: String,
|
||||
pub deleted_at: i64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_incident(data: AddIncidentData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
incident_service::add_new_incident(conn, &user_id, &data.book_id, &data.name, lang, data.incident_id.as_deref())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_incident(data: RemoveIncidentData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
incident_service::remove_incident(conn, &user_id, &data.book_id, &data.incident_id, data.deleted_at, lang)
|
||||
}
|
||||
|
||||
// ─── Plot Points ──────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddPlotPointData {
|
||||
pub book_id: String,
|
||||
pub name: String,
|
||||
pub incident_id: String,
|
||||
pub plot_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemovePlotPointData {
|
||||
pub plot_id: String,
|
||||
pub book_id: String,
|
||||
pub deleted_at: i64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_plot_point(data: AddPlotPointData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
plotpoint_service::add_new_plot_point(conn, &user_id, &data.book_id, &data.incident_id, &data.name, lang, data.plot_id.as_deref())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_plot_point(data: RemovePlotPointData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
plotpoint_service::remove_plot_point(conn, &user_id, &data.book_id, &data.plot_id, data.deleted_at, lang)
|
||||
}
|
||||
|
||||
// ─── Issues ───────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddIssueData {
|
||||
pub book_id: String,
|
||||
pub name: String,
|
||||
pub issue_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveIssueData {
|
||||
pub book_id: String,
|
||||
pub issue_id: String,
|
||||
pub deleted_at: i64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_issue(data: AddIssueData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
issue_service::add_new_issue(conn, &user_id, &data.book_id, &data.name, lang, data.issue_id.as_deref())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_issue(data: RemoveIssueData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
issue_service::remove_issue(conn, &user_id, &data.book_id, &data.issue_id, data.deleted_at, lang)
|
||||
}
|
||||
|
||||
// ─── Worlds ───────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetWorldsData {
|
||||
pub book_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddWorldData {
|
||||
pub book_id: String,
|
||||
pub world_name: String,
|
||||
pub id: Option<String>,
|
||||
pub series_world_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddWorldElementData {
|
||||
pub world_id: String,
|
||||
pub element_name: String,
|
||||
pub element_type: String,
|
||||
pub id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveWorldElementData {
|
||||
pub element_id: String,
|
||||
pub book_id: String,
|
||||
pub deleted_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateWorldDataCmd {
|
||||
pub world: world_service::WorldProps,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_worlds(data: GetWorldsData, db: State<DbManager>, session: State<SessionState>) -> Result<world_service::WorldListResponse, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
world_service::get_worlds(conn, &user_id, &data.book_id, lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_world(data: AddWorldData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
world_service::add_new_world(conn, &user_id, &data.book_id, &data.world_name, lang, data.id.as_deref(), data.series_world_id.as_deref())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_world_element(data: AddWorldElementData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
world_service::add_new_element_to_world(conn, &user_id, &data.world_id, &data.element_name, &data.element_type, lang, data.id.as_deref())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_world_element(data: RemoveWorldElementData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
world_service::remove_element_from_world(conn, &user_id, &data.book_id, &data.element_id, data.deleted_at, lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_world(data: UpdateWorldDataCmd, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
world_service::update_world(conn, &user_id, &data.world, lang)
|
||||
}
|
||||
|
||||
// ─── Guidelines ───────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetGuidelineData {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateGuidelineData {
|
||||
pub book_id: String,
|
||||
pub tone: Option<String>,
|
||||
pub atmosphere: Option<String>,
|
||||
pub writing_style: Option<String>,
|
||||
pub themes: Option<String>,
|
||||
pub symbolism: Option<String>,
|
||||
pub motifs: Option<String>,
|
||||
pub narrative_voice: Option<String>,
|
||||
pub pacing: Option<String>,
|
||||
pub intended_audience: Option<String>,
|
||||
pub key_messages: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateAIGuidelineData {
|
||||
pub book_id: String,
|
||||
pub narrative_type: i64,
|
||||
pub dialogue_type: i64,
|
||||
pub plot_summary: String,
|
||||
pub tone_atmosphere: String,
|
||||
pub verb_tense: i64,
|
||||
pub language: i64,
|
||||
pub themes: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_guideline(data: GetGuidelineData, db: State<DbManager>, session: State<SessionState>) -> Result<Option<guideline_service::GuideLineProps>, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
guideline_service::get_guide_line(conn, &user_id, &data.id, lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_guideline(data: UpdateGuidelineData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
guideline_service::update_guide_line(conn, &user_id, &data.book_id, data.tone.as_deref(), data.atmosphere.as_deref(), data.writing_style.as_deref(), data.themes.as_deref(), data.symbolism.as_deref(), data.motifs.as_deref(), data.narrative_voice.as_deref(), data.pacing.as_deref(), data.key_messages.as_deref(), data.intended_audience.as_deref(), lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_ai_guideline(data: GetGuidelineData, db: State<DbManager>, session: State<SessionState>) -> Result<guideline_service::GuideLineAI, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
guideline_service::get_guide_line_ai(conn, &user_id, &data.id, lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_ai_guideline(data: UpdateAIGuidelineData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
guideline_service::set_ai_guide_line(conn, &user_id, &data.book_id, data.narrative_type, data.dialogue_type, &data.plot_summary, &data.tone_atmosphere, data.verb_tense, data.language, &data.themes, lang)
|
||||
}
|
||||
|
||||
// ─── Export ───────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExportInfoData {
|
||||
pub book_id: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ExportBookData {
|
||||
pub book_id: String,
|
||||
pub format: String,
|
||||
pub selections: Option<Vec<crate::domains::chapter::repo::ChapterSelectionParam>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_book_export_info(data: ExportInfoData, db: State<DbManager>, session: State<SessionState>) -> Result<Vec<chapter_service::ChapterExportInfo>, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
chapter_service::get_chapters_export_info(conn, &user_id, &data.book_id, lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn export_book(data: ExportBookData, db: State<DbManager>, session: State<SessionState>) -> Result<Vec<u8>, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
let book_data = chapter_service::get_complete_book_data_with_selections(conn, &user_id, &data.book_id, data.selections.as_deref(), lang)?;
|
||||
match data.format.as_str() {
|
||||
"epub" => { let result = export_service::transform_to_epub(&book_data)?; Ok(result.buffer) },
|
||||
"pdf" => { let result = export_service::transform_to_pdf(&book_data)?; Ok(result.buffer) },
|
||||
"docx" => { let result = export_service::transform_to_docx(&book_data)?; Ok(result.buffer) },
|
||||
_ => Err(AppError::Validation(if lang == crate::shared::types::Lang::Fr { "Format non supporté.".to_string() } else { "Unsupported format.".to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Sync ─────────────────────────────────────────────
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_synced_books(db: State<DbManager>, session: State<SessionState>) -> Result<Vec<sync_service::SyncedBookFull>, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
sync_service::get_synced_books(conn, &user_id, lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn upload_book_to_server(book_id: String, db: State<DbManager>, session: State<SessionState>) -> Result<service::CompleteBook, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
upload_service::upload_book_for_sync(conn, &user_id, &book_id, lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sync_save_book(data: service::CompleteBook, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
download_service::save_complete_book(conn, &user_id, &data, lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sync_book_to_client(data: service::CompleteBook, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
sync_service::sync_book_from_server_to_client(conn, &user_id, &data, lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn sync_book_to_server(data: service::BookSyncCompare, db: State<DbManager>, session: State<SessionState>) -> Result<service::CompleteBook, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = get_conn(&db, &user_id)?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
sync_service::get_complete_sync_book(conn, &user_id, &data, lang)
|
||||
}
|
||||
3
src-tauri/src/domains/book/mod.rs
Normal file
3
src-tauri/src/domains/book/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod commands;
|
||||
pub mod repo;
|
||||
pub mod service;
|
||||
509
src-tauri/src/domains/book/repo.rs
Normal file
509
src-tauri/src/domains/book/repo.rs
Normal file
@@ -0,0 +1,509 @@
|
||||
use rusqlite::{params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
pub struct BookQuery {
|
||||
pub book_id: String,
|
||||
pub book_type: String,
|
||||
pub author_id: String,
|
||||
pub title: String,
|
||||
pub hashed_title: String,
|
||||
pub sub_title: Option<String>,
|
||||
pub hashed_sub_title: Option<String>,
|
||||
pub summary: Option<String>,
|
||||
pub serie_id: Option<i64>,
|
||||
pub desired_release_date: Option<String>,
|
||||
pub desired_word_count: Option<i64>,
|
||||
pub words_count: Option<i64>,
|
||||
pub cover_image: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EritBooksTable {
|
||||
pub book_id: String,
|
||||
pub book_type: String,
|
||||
pub author_id: String,
|
||||
pub title: String,
|
||||
pub hashed_title: String,
|
||||
pub sub_title: Option<String>,
|
||||
pub hashed_sub_title: Option<String>,
|
||||
pub summary: Option<String>,
|
||||
pub serie_id: Option<i64>,
|
||||
pub desired_release_date: Option<String>,
|
||||
pub desired_word_count: Option<i64>,
|
||||
pub words_count: Option<i64>,
|
||||
pub last_update: i64,
|
||||
pub cover_image: Option<String>,
|
||||
}
|
||||
|
||||
pub struct SyncedBookResult {
|
||||
pub book_id: String,
|
||||
pub book_type: String,
|
||||
pub title: String,
|
||||
pub sub_title: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookToolsTable {
|
||||
pub book_id: String,
|
||||
pub user_id: String,
|
||||
pub characters_enabled: i64,
|
||||
pub worlds_enabled: i64,
|
||||
pub locations_enabled: i64,
|
||||
pub spells_enabled: i64,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedBookToolsResult {
|
||||
pub last_update: i64,
|
||||
pub characters_enabled: i64,
|
||||
pub worlds_enabled: i64,
|
||||
pub locations_enabled: i64,
|
||||
pub spells_enabled: i64,
|
||||
}
|
||||
|
||||
/// Retrieves all books for a user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns list of user's books.
|
||||
pub fn fetch_books(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<BookQuery>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT book_id, type, author_id, title, sub_title, summary, serie_id, desired_release_date, desired_word_count, words_count, cover_image FROM erit_books WHERE author_id = ?1 ORDER BY book_id DESC")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la liste des livres.".to_string() } else { "Unable to retrieve book list.".to_string() }))?;
|
||||
|
||||
let books = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(BookQuery {
|
||||
book_id: query_row.get(0)?, book_type: query_row.get(1)?,
|
||||
author_id: query_row.get(2)?, title: query_row.get(3)?,
|
||||
sub_title: query_row.get(4)?, summary: query_row.get(5)?,
|
||||
serie_id: query_row.get(6)?, desired_release_date: query_row.get(7)?,
|
||||
desired_word_count: query_row.get(8)?, words_count: query_row.get(9)?,
|
||||
cover_image: query_row.get(10)?, hashed_title: String::new(),
|
||||
hashed_sub_title: None,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la liste des livres.".to_string() } else { "Unable to retrieve book list.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la liste des livres.".to_string() } else { "Unable to retrieve book list.".to_string() }))?;
|
||||
|
||||
Ok(books)
|
||||
}
|
||||
|
||||
/// Updates a book's cover image.
|
||||
/// * `conn` - Database connection
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `cover_image_name` - The cover image file name
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `last_update` - The update timestamp
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the update was successful.
|
||||
|
||||
/// Retrieves a book by its identifier.
|
||||
/// * `conn` - Database connection
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the book information.
|
||||
pub fn fetch_book(conn: &Connection, book_id: &str, user_id: &str, lang: Lang) -> AppResult<BookQuery> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT book_id, author_id, title, summary, sub_title, cover_image, desired_release_date, desired_word_count, words_count, serie_id FROM erit_books WHERE book_id=?1 AND author_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les informations du livre.".to_string() } else { "Unable to retrieve book information.".to_string() }))?;
|
||||
|
||||
let book = statement
|
||||
.query_row(params![book_id, user_id], |query_row| {
|
||||
Ok(BookQuery {
|
||||
book_id: query_row.get(0)?, author_id: query_row.get(1)?,
|
||||
title: query_row.get(2)?, summary: query_row.get(3)?,
|
||||
sub_title: query_row.get(4)?, cover_image: query_row.get(5)?,
|
||||
desired_release_date: query_row.get(6)?, desired_word_count: query_row.get(7)?,
|
||||
words_count: query_row.get(8)?, serie_id: query_row.get(9)?,
|
||||
book_type: String::new(), hashed_title: String::new(),
|
||||
hashed_sub_title: None,
|
||||
})
|
||||
})
|
||||
.map_err(|error| match error {
|
||||
rusqlite::Error::QueryReturnedNoRows => AppError::NotFound(if lang == Lang::Fr { "Livre non trouvé.".to_string() } else { "Book not found.".to_string() }),
|
||||
_ => AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les informations du livre.".to_string() } else { "Unable to retrieve book information.".to_string() }),
|
||||
})?;
|
||||
|
||||
Ok(book)
|
||||
}
|
||||
|
||||
/// Verifies if a book already exists for a user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `hashed_title` - The hashed book title
|
||||
/// * `hashed_sub_title` - The hashed book subtitle
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the book exists.
|
||||
pub fn verify_book_exist(conn: &Connection, hashed_title: &str, hashed_sub_title: &str, user_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT book_id FROM erit_books WHERE hashed_title=?1 AND author_id=?2 AND hashed_sub_title=?3")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du livre.".to_string() } else { "Unable to verify book existence.".to_string() }))?;
|
||||
|
||||
let exists = statement
|
||||
.exists(params![hashed_title, user_id, hashed_sub_title])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du livre.".to_string() } else { "Unable to verify book existence.".to_string() }))?;
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Inserts a new book into the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `encrypted_title` - The encrypted title
|
||||
/// * `hashed_title` - The hashed title
|
||||
/// * `encrypted_sub_title` - The encrypted subtitle
|
||||
/// * `hashed_sub_title` - The hashed subtitle
|
||||
/// * `encrypted_summary` - The encrypted summary
|
||||
/// * `book_type` - The book type
|
||||
/// * `serie` - The series identifier
|
||||
/// * `publication_date` - The desired publication date
|
||||
/// * `desired_word_count` - The desired word count
|
||||
/// * `last_update` - The creation timestamp
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the created book identifier.
|
||||
pub fn insert_book(
|
||||
conn: &Connection, book_id: &str, user_id: &str, encrypted_title: &str, hashed_title: &str,
|
||||
encrypted_sub_title: &str, hashed_sub_title: &str, encrypted_summary: &str, book_type: &str,
|
||||
serie: i64, publication_date: Option<&str>, desired_word_count: i64, last_update: i64, lang: Lang,
|
||||
) -> AppResult<String> {
|
||||
let insert_result = conn
|
||||
.execute("INSERT INTO erit_books (book_id, type, author_id, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, desired_word_count, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)", params![book_id, book_type, user_id, encrypted_title, hashed_title, encrypted_sub_title, hashed_sub_title, encrypted_summary, serie, publication_date, desired_word_count, last_update])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le livre.".to_string() } else { "Unable to add book.".to_string() }))?;
|
||||
|
||||
if insert_result > 0 {
|
||||
Ok(book_id.to_string())
|
||||
} else {
|
||||
Err(AppError::Internal(if lang == Lang::Fr { "Erreur lors de l'ajout du livre.".to_string() } else { "Error adding book.".to_string() }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves a book's cover image.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the cover information.
|
||||
|
||||
/// Updates a book's basic information.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `title` - The new title
|
||||
/// * `hashed_title` - The hashed title
|
||||
/// * `sub_title` - The new subtitle
|
||||
/// * `hashed_sub_title` - The hashed subtitle
|
||||
/// * `summary` - The new summary
|
||||
/// * `publication_date` - The new publication date
|
||||
/// * `word_count` - The new desired word count
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `last_update` - The update timestamp
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the update was successful.
|
||||
pub fn update_book_basic_information(
|
||||
conn: &Connection, user_id: &str, title: &str, hashed_title: &str, sub_title: &str,
|
||||
hashed_sub_title: &str, summary: &str, publication_date: Option<&str>, word_count: i64,
|
||||
book_id: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute("UPDATE erit_books SET title=?1, hashed_title=?2, sub_title=?3, hashed_sub_title=?4, summary=?5, serie_id=?6, desired_release_date=?7, desired_word_count=?8, last_update=?9 WHERE author_id=?10 AND book_id=?11", params![title, hashed_title, sub_title, hashed_sub_title, summary, 0, publication_date, word_count, last_update, user_id, book_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour les informations du livre.".to_string() } else { "Unable to update book information.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Deletes a book from the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `book_id` - The book identifier to delete
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the deletion was successful.
|
||||
pub fn delete_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let delete_result = conn
|
||||
.execute("DELETE FROM erit_books WHERE author_id=?1 AND book_id=?2", params![user_id, book_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le livre.".to_string() } else { "Unable to delete book.".to_string() }))?;
|
||||
|
||||
Ok(delete_result > 0)
|
||||
}
|
||||
|
||||
/// Retrieves all columns from erit_books table for a book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the complete book data.
|
||||
pub fn fetch_erit_books_table(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<EritBooksTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT book_id, type, author_id, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, desired_word_count, words_count, cover_image, last_update FROM erit_books WHERE book_id=?1 AND author_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les informations du livre.".to_string() } else { "Unable to retrieve book information.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![book_id, user_id], |query_row| {
|
||||
Ok(EritBooksTable {
|
||||
book_id: query_row.get(0)?, book_type: query_row.get(1)?,
|
||||
author_id: query_row.get(2)?, title: query_row.get(3)?,
|
||||
hashed_title: query_row.get(4)?, sub_title: query_row.get(5)?,
|
||||
hashed_sub_title: query_row.get(6)?, summary: query_row.get(7)?,
|
||||
serie_id: query_row.get(8)?, desired_release_date: query_row.get(9)?,
|
||||
desired_word_count: query_row.get(10)?, words_count: query_row.get(11)?,
|
||||
cover_image: query_row.get(12)?, last_update: query_row.get(13)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les informations du livre.".to_string() } else { "Unable to retrieve book information.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les informations du livre.".to_string() } else { "Unable to retrieve book information.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Retrieves synced books for a user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns list of books with sync information.
|
||||
pub fn fetch_synced_books(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedBookResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT book_id, type, title, sub_title, last_update FROM erit_books WHERE author_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les livres synchronisés.".to_string() } else { "Unable to retrieve synced books.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedBookResult {
|
||||
book_id: query_row.get(0)?, book_type: query_row.get(1)?,
|
||||
title: query_row.get(2)?, sub_title: query_row.get(3)?,
|
||||
last_update: query_row.get(4)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les livres synchronisés.".to_string() } else { "Unable to retrieve synced books.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les livres synchronisés.".to_string() } else { "Unable to retrieve synced books.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Inserts a synced book from the server.
|
||||
/// * `conn` - Database connection
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `book_type` - The book type
|
||||
/// * `title` - The encrypted title
|
||||
/// * `hashed_title` - The hashed title
|
||||
/// * `sub_title` - The encrypted subtitle
|
||||
/// * `hashed_sub_title` - The hashed subtitle
|
||||
/// * `summary` - The encrypted summary
|
||||
/// * `serie_id` - The series identifier
|
||||
/// * `desired_release_date` - The desired release date
|
||||
/// * `desired_word_count` - The desired word count
|
||||
/// * `words_count` - The current word count
|
||||
/// * `cover_image` - The cover image file name
|
||||
/// * `last_update` - The last update timestamp
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the insertion was successful.
|
||||
pub fn insert_sync_book(
|
||||
conn: &Connection, book_id: &str, user_id: &str, book_type: &str, title: &str,
|
||||
hashed_title: &str, sub_title: Option<&str>, hashed_sub_title: Option<&str>,
|
||||
summary: Option<&str>, serie_id: Option<i64>, desired_release_date: Option<&str>,
|
||||
desired_word_count: Option<i64>, words_count: Option<i64>, cover_image: Option<&str>,
|
||||
last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute("INSERT INTO erit_books (book_id, author_id, type, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, desired_word_count, words_count, cover_image, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", params![book_id, user_id, book_type, title, hashed_title, sub_title, hashed_sub_title, summary, serie_id, desired_release_date, desired_word_count, words_count, cover_image, last_update])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le livre synchronisé.".to_string() } else { "Unable to insert synced book.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Retrieves a complete book by its identifier (without author verification).
|
||||
/// * `conn` - Database connection
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the complete book data.
|
||||
pub fn fetch_complete_book_by_id(conn: &Connection, book_id: &str, lang: Lang) -> AppResult<Vec<EritBooksTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT * FROM erit_books WHERE book_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le livre complet.".to_string() } else { "Unable to retrieve complete book.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![book_id], |query_row| {
|
||||
Ok(EritBooksTable {
|
||||
book_id: query_row.get(0)?, book_type: query_row.get(1)?,
|
||||
author_id: query_row.get(2)?, title: query_row.get(3)?,
|
||||
hashed_title: query_row.get(4)?, sub_title: query_row.get(5)?,
|
||||
hashed_sub_title: query_row.get(6)?, summary: query_row.get(7)?,
|
||||
serie_id: query_row.get(8)?, desired_release_date: query_row.get(9)?,
|
||||
desired_word_count: query_row.get(10)?, words_count: query_row.get(11)?,
|
||||
cover_image: query_row.get(12)?, last_update: query_row.get(13)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le livre complet.".to_string() } else { "Unable to retrieve complete book.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le livre complet.".to_string() } else { "Unable to retrieve complete book.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Retrieves the book tools settings for a user and book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the book tools settings or None if not found.
|
||||
pub fn fetch_book_tools(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Option<BookToolsTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, spells_enabled, last_update FROM book_tools WHERE user_id=?1 AND book_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les paramètres des outils.".to_string() } else { "Unable to fetch tools settings.".to_string() }))?;
|
||||
|
||||
let result = statement
|
||||
.query_row(params![user_id, book_id], |query_row| {
|
||||
Ok(BookToolsTable {
|
||||
book_id: query_row.get(0)?, user_id: query_row.get(1)?,
|
||||
characters_enabled: query_row.get(2)?, worlds_enabled: query_row.get(3)?,
|
||||
locations_enabled: query_row.get(4)?, spells_enabled: query_row.get(5)?,
|
||||
last_update: query_row.get(6)?,
|
||||
})
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(row) => Ok(Some(row)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les paramètres des outils.".to_string() } else { "Unable to fetch tools settings.".to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates a book tool setting. If no row exists, inserts a new one.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `tool_name` - The tool column name (characters_enabled, worlds_enabled, locations_enabled, spells_enabled)
|
||||
/// * `enabled` - Whether the tool is enabled
|
||||
/// * `last_update` - The update timestamp
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the update or insert was successful.
|
||||
pub fn update_book_tool_setting(conn: &Connection, user_id: &str, book_id: &str, tool_name: &str, enabled: bool, last_update: i64, lang: Lang) -> AppResult<bool> {
|
||||
let enabled_value: i64 = if enabled { 1 } else { 0 };
|
||||
|
||||
let update_query = format!("UPDATE book_tools SET {}=?1, last_update=?2 WHERE user_id=?3 AND book_id=?4", tool_name);
|
||||
let update_result = conn
|
||||
.execute(&update_query, params![enabled_value, last_update, user_id, book_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour les paramètres des outils.".to_string() } else { "Unable to update tools settings.".to_string() }))?;
|
||||
|
||||
if update_result > 0 {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let characters_value: i64 = if tool_name == "characters_enabled" { enabled_value } else { 0 };
|
||||
let worlds_value: i64 = if tool_name == "worlds_enabled" { enabled_value } else { 0 };
|
||||
let locations_value: i64 = if tool_name == "locations_enabled" { enabled_value } else { 0 };
|
||||
let spells_value: i64 = if tool_name == "spells_enabled" { enabled_value } else { 0 };
|
||||
|
||||
let insert_result = conn
|
||||
.execute("INSERT INTO book_tools (book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, spells_enabled, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", params![book_id, user_id, characters_value, worlds_value, locations_value, spells_value, last_update])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour les paramètres des outils.".to_string() } else { "Unable to update tools settings.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Upserts book tools settings during sync.
|
||||
/// Inserts if not exists, updates if exists.
|
||||
/// * `conn` - Database connection
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `characters_enabled` - Whether characters tool is enabled
|
||||
/// * `worlds_enabled` - Whether worlds tool is enabled
|
||||
/// * `locations_enabled` - Whether locations tool is enabled
|
||||
/// * `spells_enabled` - Whether spells tool is enabled
|
||||
/// * `last_update` - The last update timestamp
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the upsert was successful, false on error.
|
||||
pub fn insert_sync_book_tools(conn: &Connection, book_id: &str, user_id: &str, characters_enabled: i64, worlds_enabled: i64, locations_enabled: i64, spells_enabled: i64, last_update: i64, _lang: Lang) -> bool {
|
||||
let result = conn.execute("INSERT INTO book_tools (book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, spells_enabled, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT (book_id, user_id) DO UPDATE SET characters_enabled = excluded.characters_enabled, worlds_enabled = excluded.worlds_enabled, locations_enabled = excluded.locations_enabled, spells_enabled = excluded.spells_enabled, last_update = excluded.last_update", params![book_id, user_id, characters_enabled, worlds_enabled, locations_enabled, spells_enabled, last_update]);
|
||||
|
||||
match result {
|
||||
Ok(_) => true,
|
||||
Err(error) => {
|
||||
eprintln!("[BookRepository] DB Error: {}", error);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves synced book tools for a user and book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the synced book tools settings or None if not found.
|
||||
pub fn fetch_synced_book_tools(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Option<SyncedBookToolsResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT last_update, characters_enabled, worlds_enabled, locations_enabled, spells_enabled FROM book_tools WHERE user_id = ?1 AND book_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les paramètres des outils.".to_string() } else { "Unable to fetch tools settings.".to_string() }))?;
|
||||
|
||||
let result = statement
|
||||
.query_row(params![user_id, book_id], |query_row| {
|
||||
Ok(SyncedBookToolsResult {
|
||||
last_update: query_row.get(0)?, characters_enabled: query_row.get(1)?,
|
||||
worlds_enabled: query_row.get(2)?, locations_enabled: query_row.get(3)?,
|
||||
spells_enabled: query_row.get(4)?,
|
||||
})
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(row) => Ok(Some(row)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les paramètres des outils.".to_string() } else { "Unable to fetch tools settings.".to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a book exists for a user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the book exists, false otherwise.
|
||||
pub fn is_book_exist(conn: &Connection, user_id: &str, book_id: &str, _lang: Lang) -> bool {
|
||||
let result = conn.prepare("SELECT 1 FROM erit_books WHERE author_id = ?1 AND book_id = ?2 LIMIT 1");
|
||||
|
||||
match result {
|
||||
Ok(mut statement) => {
|
||||
statement.exists(params![user_id, book_id]).unwrap_or(false)
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("DB Error: {}", error);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves the series_id for a book from series_books table.
|
||||
/// * `conn` - Database connection
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the series_id or None if book is not in a series.
|
||||
pub fn fetch_book_series_id(conn: &Connection, book_id: &str, _lang: Lang) -> Option<String> {
|
||||
let result = conn.prepare("SELECT series_id FROM series_books WHERE book_id = ?1 LIMIT 1");
|
||||
|
||||
match result {
|
||||
Ok(mut statement) => {
|
||||
let query_result = statement.query_row(params![book_id], |query_row| {
|
||||
query_row.get::<_, String>(0)
|
||||
});
|
||||
match query_result {
|
||||
Ok(series_id) => if series_id.is_empty() { None } else { Some(series_id) },
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
eprintln!("DB Error: {}", error);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
975
src-tauri/src/domains/book/service.rs
Normal file
975
src-tauri/src/domains/book/service.rs
Normal file
@@ -0,0 +1,975 @@
|
||||
use rusqlite::Connection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::crypto::encryption::{encrypt_data_with_user_key, decrypt_data_with_user_key, hash_element};
|
||||
use crate::crypto::key_manager::get_user_encryption_key;
|
||||
use crate::domains::book::repo;
|
||||
use crate::domains::chapter::repo as chapter_repo;
|
||||
use crate::domains::chapter::service as chapter_service;
|
||||
use crate::domains::tombstone::repo as tombstone_repo;
|
||||
use crate::domains::user::repo as user_repo;
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::helpers::{create_unique_id, timestamp_in_seconds};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedBookTools {
|
||||
pub last_update: i64,
|
||||
pub characters_enabled: bool,
|
||||
pub worlds_enabled: bool,
|
||||
pub locations_enabled: bool,
|
||||
pub spells_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookToolsSettings {
|
||||
pub characters: bool,
|
||||
pub worlds: bool,
|
||||
pub locations: bool,
|
||||
pub spells: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookProps {
|
||||
pub book_id: String,
|
||||
pub book_type: String,
|
||||
pub author_id: String,
|
||||
pub title: String,
|
||||
pub sub_title: String,
|
||||
pub summary: String,
|
||||
pub serie_id: i64,
|
||||
pub series_id: Option<String>,
|
||||
pub desired_release_date: String,
|
||||
pub desired_word_count: i64,
|
||||
pub word_count: i64,
|
||||
pub cover_image: String,
|
||||
pub book_meta: Option<String>,
|
||||
pub tools: Option<BookToolsSettings>,
|
||||
}
|
||||
|
||||
pub use chapter_service::CompleteChapterContent;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CompleteBookData {
|
||||
pub book_id: String,
|
||||
pub title: String,
|
||||
pub sub_title: String,
|
||||
pub summary: String,
|
||||
pub cover_image: String,
|
||||
pub user_infos: BookUserInfos,
|
||||
pub chapters: Vec<CompleteChapterContent>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookUserInfos {
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub author_name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedBook {
|
||||
pub id: String,
|
||||
pub book_type: String,
|
||||
pub title: String,
|
||||
pub sub_title: Option<String>,
|
||||
pub last_update: i64,
|
||||
pub chapters: Vec<SyncedChapter>,
|
||||
pub characters: Vec<SyncedCharacter>,
|
||||
pub locations: Vec<SyncedLocation>,
|
||||
pub worlds: Vec<SyncedWorld>,
|
||||
pub incidents: Vec<SyncedIncident>,
|
||||
pub plot_points: Vec<SyncedPlotPoint>,
|
||||
pub issues: Vec<SyncedIssue>,
|
||||
pub act_summaries: Vec<SyncedActSummary>,
|
||||
pub guide_line: Option<SyncedGuideLine>,
|
||||
pub ai_guide_line: Option<SyncedAIGuideLine>,
|
||||
pub book_tools: Option<SyncedBookTools>,
|
||||
pub spells: Vec<SyncedSpell>,
|
||||
pub spell_tags: Vec<SyncedSpellTag>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookSyncCompare {
|
||||
pub id: String,
|
||||
pub chapters: Vec<String>,
|
||||
pub chapter_contents: Vec<String>,
|
||||
pub chapter_infos: Vec<String>,
|
||||
pub characters: Vec<String>,
|
||||
pub character_attributes: Vec<String>,
|
||||
pub locations: Vec<String>,
|
||||
pub location_elements: Vec<String>,
|
||||
pub location_sub_elements: Vec<String>,
|
||||
pub worlds: Vec<String>,
|
||||
pub world_elements: Vec<String>,
|
||||
pub incidents: Vec<String>,
|
||||
pub plot_points: Vec<String>,
|
||||
pub issues: Vec<String>,
|
||||
pub act_summaries: Vec<String>,
|
||||
pub guide_line: bool,
|
||||
pub ai_guide_line: bool,
|
||||
pub book_tools: bool,
|
||||
pub spells: Vec<String>,
|
||||
pub spell_tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedChapter {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedCharacter {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedLocation {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedWorld {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedIncident {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedPlotPoint {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedIssue {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedActSummary {
|
||||
pub id: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SyncedGuideLine {
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SyncedAIGuideLine {
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedSpell {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedSpellTag {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CompleteBook {
|
||||
pub erit_books: Vec<repo::EritBooksTable>,
|
||||
pub act_summaries: Vec<BookActSummariesTable>,
|
||||
pub ai_guide_line: Vec<BookAIGuideLineTable>,
|
||||
pub chapters: Vec<BookChaptersTable>,
|
||||
pub chapter_contents: Vec<BookChapterContentTable>,
|
||||
pub chapter_infos: Vec<BookChapterInfosTable>,
|
||||
pub characters: Vec<BookCharactersTable>,
|
||||
pub character_attributes: Vec<BookCharactersAttributesTable>,
|
||||
pub guide_line: Vec<BookGuideLineTable>,
|
||||
pub incidents: Vec<BookIncidentsTable>,
|
||||
pub issues: Vec<BookIssuesTable>,
|
||||
pub locations: Vec<BookLocationTable>,
|
||||
pub plot_points: Vec<BookPlotPointsTable>,
|
||||
pub worlds: Vec<BookWorldTable>,
|
||||
pub world_elements: Vec<BookWorldElementsTable>,
|
||||
pub location_elements: Vec<LocationElementTable>,
|
||||
pub location_sub_elements: Vec<LocationSubElementTable>,
|
||||
pub book_tools: Vec<repo::BookToolsTable>,
|
||||
pub spells: Vec<BookSpellsTable>,
|
||||
pub spell_tags: Vec<BookSpellTagsTable>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookActSummariesTable {
|
||||
pub summary_id: String,
|
||||
pub book_id: String,
|
||||
pub user_id: String,
|
||||
pub act_number: i64,
|
||||
pub summary: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookAIGuideLineTable {
|
||||
pub user_id: String,
|
||||
pub book_id: String,
|
||||
pub global_resume: Option<String>,
|
||||
pub themes: Option<String>,
|
||||
pub verbe_tense: Option<i64>,
|
||||
pub narrative_type: Option<i64>,
|
||||
pub langue: Option<i64>,
|
||||
pub dialogue_type: Option<i64>,
|
||||
pub tone: Option<String>,
|
||||
pub atmosphere: Option<String>,
|
||||
pub current_resume: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookChaptersTable {
|
||||
pub chapter_id: String,
|
||||
pub book_id: String,
|
||||
pub user_id: String,
|
||||
pub title: String,
|
||||
pub hashed_title: String,
|
||||
pub chapter_order: i64,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookChapterContentTable {
|
||||
pub content_id: String,
|
||||
pub chapter_id: String,
|
||||
pub user_id: String,
|
||||
pub content: Option<String>,
|
||||
pub version: i64,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookChapterInfosTable {
|
||||
pub chapter_id: String,
|
||||
pub user_id: String,
|
||||
pub summary: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookCharactersTable {
|
||||
pub character_id: String,
|
||||
pub book_id: String,
|
||||
pub user_id: String,
|
||||
pub first_name: String,
|
||||
pub last_name: Option<String>,
|
||||
pub nickname: Option<String>,
|
||||
pub age: Option<i64>,
|
||||
pub gender: Option<String>,
|
||||
pub species: Option<String>,
|
||||
pub nationality: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub category: String,
|
||||
pub image: Option<String>,
|
||||
pub role: Option<String>,
|
||||
pub biography: Option<String>,
|
||||
pub history: Option<String>,
|
||||
pub speech_pattern: Option<String>,
|
||||
pub catchphrase: Option<String>,
|
||||
pub residence: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookCharactersAttributesTable {
|
||||
pub attr_id: String,
|
||||
pub character_id: String,
|
||||
pub user_id: String,
|
||||
pub attribute_name: String,
|
||||
pub attribute_value: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookGuideLineTable {
|
||||
pub user_id: String,
|
||||
pub book_id: String,
|
||||
pub tone: Option<String>,
|
||||
pub atmosphere: Option<String>,
|
||||
pub writing_style: Option<String>,
|
||||
pub themes: Option<String>,
|
||||
pub symbolism: Option<String>,
|
||||
pub motifs: Option<String>,
|
||||
pub narrative_voice: Option<String>,
|
||||
pub pacing: Option<String>,
|
||||
pub intended_audience: Option<String>,
|
||||
pub key_messages: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookIncidentsTable {
|
||||
pub incident_id: String,
|
||||
pub chapter_id: String,
|
||||
pub user_id: String,
|
||||
pub name: String,
|
||||
pub hashed_name: String,
|
||||
pub description: Option<String>,
|
||||
pub incident_order: i64,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookIssuesTable {
|
||||
pub issue_id: String,
|
||||
pub chapter_id: String,
|
||||
pub user_id: String,
|
||||
pub name: String,
|
||||
pub hashed_name: String,
|
||||
pub description: Option<String>,
|
||||
pub issue_order: i64,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookLocationTable {
|
||||
pub loc_id: String,
|
||||
pub book_id: String,
|
||||
pub user_id: String,
|
||||
pub loc_name: String,
|
||||
pub loc_original_name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookPlotPointsTable {
|
||||
pub plot_point_id: String,
|
||||
pub chapter_id: String,
|
||||
pub user_id: String,
|
||||
pub name: String,
|
||||
pub hashed_name: String,
|
||||
pub description: Option<String>,
|
||||
pub plot_point_order: i64,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookWorldTable {
|
||||
pub world_id: String,
|
||||
pub book_id: String,
|
||||
pub user_id: String,
|
||||
pub name: String,
|
||||
pub hashed_name: String,
|
||||
pub history: Option<String>,
|
||||
pub politics: Option<String>,
|
||||
pub economy: Option<String>,
|
||||
pub religion: Option<String>,
|
||||
pub languages: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookWorldElementsTable {
|
||||
pub element_id: String,
|
||||
pub world_id: String,
|
||||
pub user_id: String,
|
||||
pub element_type: i64,
|
||||
pub name: String,
|
||||
pub original_name: String,
|
||||
pub description: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LocationElementTable {
|
||||
pub element_id: String,
|
||||
pub location_id: String,
|
||||
pub user_id: String,
|
||||
pub element_name: String,
|
||||
pub original_name: String,
|
||||
pub element_description: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LocationSubElementTable {
|
||||
pub sub_element_id: String,
|
||||
pub element_id: String,
|
||||
pub user_id: String,
|
||||
pub sub_elem_name: String,
|
||||
pub original_name: String,
|
||||
pub sub_elem_description: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookSpellsTable {
|
||||
pub spell_id: String,
|
||||
pub book_id: String,
|
||||
pub user_id: String,
|
||||
pub name: String,
|
||||
pub name_hash: String,
|
||||
pub description: String,
|
||||
pub appearance: String,
|
||||
pub tags: String,
|
||||
pub power_level: Option<String>,
|
||||
pub components: Option<String>,
|
||||
pub limitations: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookSpellTagsTable {
|
||||
pub tag_id: String,
|
||||
pub book_id: String,
|
||||
pub user_id: String,
|
||||
pub name: String,
|
||||
pub hashed_name: String,
|
||||
pub color: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
// ===== SERIES TABLE INTERFACES (for sync) =====
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesTable {
|
||||
pub series_id: String,
|
||||
pub user_id: String,
|
||||
pub name: String,
|
||||
pub hashed_name: String,
|
||||
pub description: Option<String>,
|
||||
pub cover_image: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesBooksTable {
|
||||
pub series_id: String,
|
||||
pub book_id: String,
|
||||
pub book_order: i64,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesCharactersTable {
|
||||
pub character_id: String,
|
||||
pub series_id: String,
|
||||
pub user_id: String,
|
||||
pub first_name: String,
|
||||
pub last_name: Option<String>,
|
||||
pub nickname: Option<String>,
|
||||
pub age: Option<i64>,
|
||||
pub gender: Option<String>,
|
||||
pub species: Option<String>,
|
||||
pub nationality: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub category: String,
|
||||
pub image: Option<String>,
|
||||
pub role: Option<String>,
|
||||
pub biography: Option<String>,
|
||||
pub history: Option<String>,
|
||||
pub speech_pattern: Option<String>,
|
||||
pub catchphrase: Option<String>,
|
||||
pub residence: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesCharacterAttributesTable {
|
||||
pub attr_id: String,
|
||||
pub character_id: String,
|
||||
pub user_id: String,
|
||||
pub attribute_name: String,
|
||||
pub attribute_value: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesWorldsTable {
|
||||
pub world_id: String,
|
||||
pub series_id: String,
|
||||
pub user_id: String,
|
||||
pub name: String,
|
||||
pub hashed_name: String,
|
||||
pub history: Option<String>,
|
||||
pub politics: Option<String>,
|
||||
pub economy: Option<String>,
|
||||
pub religion: Option<String>,
|
||||
pub languages: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesWorldElementsTable {
|
||||
pub element_id: String,
|
||||
pub world_id: String,
|
||||
pub user_id: String,
|
||||
pub element_type: i64,
|
||||
pub name: String,
|
||||
pub original_name: String,
|
||||
pub description: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesLocationsTable {
|
||||
pub loc_id: String,
|
||||
pub series_id: String,
|
||||
pub user_id: String,
|
||||
pub loc_name: String,
|
||||
pub loc_original_name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesLocationElementsTable {
|
||||
pub element_id: String,
|
||||
pub location_id: String,
|
||||
pub user_id: String,
|
||||
pub element_name: String,
|
||||
pub original_name: String,
|
||||
pub element_description: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesLocationSubElementsTable {
|
||||
pub sub_element_id: String,
|
||||
pub element_id: String,
|
||||
pub user_id: String,
|
||||
pub sub_elem_name: String,
|
||||
pub original_name: String,
|
||||
pub sub_elem_description: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesSpellsTable {
|
||||
pub spell_id: String,
|
||||
pub series_id: String,
|
||||
pub user_id: String,
|
||||
pub name: String,
|
||||
pub name_hash: String,
|
||||
pub description: String,
|
||||
pub appearance: String,
|
||||
pub tags: String,
|
||||
pub power_level: Option<String>,
|
||||
pub components: Option<String>,
|
||||
pub limitations: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesSpellTagsTable {
|
||||
pub tag_id: String,
|
||||
pub series_id: String,
|
||||
pub user_id: String,
|
||||
pub name: String,
|
||||
pub hashed_name: String,
|
||||
pub color: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CompleteSeries {
|
||||
pub series: Vec<SeriesTable>,
|
||||
pub series_books: Vec<SeriesBooksTable>,
|
||||
pub series_characters: Vec<SeriesCharactersTable>,
|
||||
pub series_character_attributes: Vec<SeriesCharacterAttributesTable>,
|
||||
pub series_worlds: Vec<SeriesWorldsTable>,
|
||||
pub series_world_elements: Vec<SeriesWorldElementsTable>,
|
||||
pub series_locations: Vec<SeriesLocationsTable>,
|
||||
pub series_location_elements: Vec<SeriesLocationElementsTable>,
|
||||
pub series_location_sub_elements: Vec<SeriesLocationSubElementsTable>,
|
||||
pub series_spells: Vec<SeriesSpellsTable>,
|
||||
pub series_spell_tags: Vec<SeriesSpellTagsTable>,
|
||||
}
|
||||
|
||||
// ===== SYNCED SERIES INTERFACES (lightweight, for comparison) =====
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedSeriesBook {
|
||||
pub book_id: String,
|
||||
pub order: i64,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedSeriesCharacterAttribute {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedSeriesCharacter {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
pub attributes: Vec<SyncedSeriesCharacterAttribute>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedSeriesWorldElement {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedSeriesWorld {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
pub elements: Vec<SyncedSeriesWorldElement>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedSeriesLocationSubElement {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedSeriesLocationElement {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
pub sub_elements: Vec<SyncedSeriesLocationSubElement>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedSeriesLocation {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
pub elements: Vec<SyncedSeriesLocationElement>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedSeriesSpell {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedSeriesSpellTag {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedSeries {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub last_update: i64,
|
||||
pub books: Vec<SyncedSeriesBook>,
|
||||
pub characters: Vec<SyncedSeriesCharacter>,
|
||||
pub worlds: Vec<SyncedSeriesWorld>,
|
||||
pub locations: Vec<SyncedSeriesLocation>,
|
||||
pub spells: Vec<SyncedSeriesSpell>,
|
||||
pub spell_tags: Vec<SyncedSeriesSpellTag>,
|
||||
}
|
||||
|
||||
// ===== FUNCTIONS =====
|
||||
|
||||
/// Retrieves all books for a specific user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns a list of decrypted book properties.
|
||||
/// Errors if the user encryption key is not found.
|
||||
pub fn get_books(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<BookProps>> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let books: Vec<repo::BookQuery> = repo::fetch_books(conn, user_id, lang)?;
|
||||
|
||||
if books.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut book_props_list: Vec<BookProps> = Vec::with_capacity(books.len());
|
||||
for book in books {
|
||||
let decrypted_title: String = decrypt_data_with_user_key(&book.title, &user_key)?;
|
||||
let decrypted_sub_title: String = if let Some(ref sub_title) = book.sub_title { decrypt_data_with_user_key(sub_title, &user_key)? } else { String::new() };
|
||||
let decrypted_summary: String = if let Some(ref summary) = book.summary { decrypt_data_with_user_key(summary, &user_key)? } else { String::new() };
|
||||
|
||||
book_props_list.push(BookProps {
|
||||
book_id: book.book_id,
|
||||
book_type: book.book_type,
|
||||
author_id: book.author_id,
|
||||
title: decrypted_title,
|
||||
sub_title: decrypted_sub_title,
|
||||
summary: decrypted_summary,
|
||||
serie_id: book.serie_id.unwrap_or(0),
|
||||
series_id: None,
|
||||
desired_release_date: book.desired_release_date.unwrap_or_default(),
|
||||
desired_word_count: book.desired_word_count.unwrap_or(0),
|
||||
word_count: book.words_count.unwrap_or(0),
|
||||
cover_image: book.cover_image.unwrap_or_default(),
|
||||
book_meta: None,
|
||||
tools: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(book_props_list)
|
||||
}
|
||||
|
||||
/// Adds a new book to the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `book_id` - The unique identifier for the book (optional, will be generated if None)
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `title` - The title of the book
|
||||
/// * `sub_title` - The subtitle of the book
|
||||
/// * `summary` - The summary of the book
|
||||
/// * `book_type` - The type/genre of the book
|
||||
/// * `serie` - The series identifier
|
||||
/// * `publication_date` - The desired publication date
|
||||
/// * `desired_word_count` - The target word count
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the book ID.
|
||||
/// Errors if a book with the same title already exists.
|
||||
pub fn add_book(
|
||||
conn: &Connection, book_id: Option<&str>, user_id: &str, title: &str, sub_title: &str,
|
||||
summary: &str, book_type: &str, serie: i64, publication_date: Option<&str>,
|
||||
desired_word_count: i64, lang: Lang,
|
||||
) -> AppResult<String> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let encrypted_title: String = encrypt_data_with_user_key(title, &user_key)?;
|
||||
let encrypted_sub_title: String = if sub_title.is_empty() { String::new() } else { encrypt_data_with_user_key(sub_title, &user_key)? };
|
||||
let encrypted_summary: String = if summary.is_empty() { String::new() } else { encrypt_data_with_user_key(summary, &user_key)? };
|
||||
let hashed_title: String = hash_element(title);
|
||||
let hashed_sub_title: String = if sub_title.is_empty() { String::new() } else { hash_element(sub_title) };
|
||||
|
||||
let book_already_exists: bool = repo::verify_book_exist(conn, &hashed_title, &hashed_sub_title, user_id, lang)?;
|
||||
if book_already_exists {
|
||||
return Err(AppError::Internal(if lang == Lang::Fr { format!("Tu as déjà un livre intitulé {} - {}.", title, sub_title) } else { format!("You already have a book named {} - {}.", title, sub_title) }));
|
||||
}
|
||||
|
||||
let final_book_id: String = create_unique_id(book_id);
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
repo::insert_book(conn, &final_book_id, user_id, &encrypted_title, &hashed_title, &encrypted_sub_title, &hashed_sub_title, &encrypted_summary, book_type, serie, publication_date, desired_word_count, last_update, lang)
|
||||
}
|
||||
|
||||
/// Retrieves a single book by its ID.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the decrypted book properties with tools settings.
|
||||
pub fn get_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<BookProps> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let book_data: repo::BookQuery = repo::fetch_book(conn, book_id, user_id, lang)?;
|
||||
let book_tools: Option<repo::BookToolsTable> = repo::fetch_book_tools(conn, user_id, book_id, lang)?;
|
||||
let series_id: Option<String> = repo::fetch_book_series_id(conn, book_id, lang);
|
||||
|
||||
let decrypted_title: String = decrypt_data_with_user_key(&book_data.title, &user_key)?;
|
||||
let decrypted_sub_title: String = if let Some(ref sub_title) = book_data.sub_title { decrypt_data_with_user_key(sub_title, &user_key)? } else { String::new() };
|
||||
let decrypted_summary: String = if let Some(ref summary) = book_data.summary { decrypt_data_with_user_key(summary, &user_key)? } else { String::new() };
|
||||
|
||||
Ok(BookProps {
|
||||
book_id: book_data.book_id,
|
||||
book_type: book_data.book_type,
|
||||
author_id: book_data.author_id,
|
||||
title: decrypted_title,
|
||||
sub_title: decrypted_sub_title,
|
||||
summary: decrypted_summary,
|
||||
serie_id: book_data.serie_id.unwrap_or(0),
|
||||
series_id,
|
||||
desired_release_date: book_data.desired_release_date.unwrap_or_default(),
|
||||
desired_word_count: book_data.desired_word_count.unwrap_or(0),
|
||||
word_count: book_data.words_count.unwrap_or(0),
|
||||
cover_image: book_data.cover_image.unwrap_or_default(),
|
||||
book_meta: None,
|
||||
tools: Some(BookToolsSettings {
|
||||
characters: book_tools.as_ref().map_or(false, |book_tools_row| book_tools_row.characters_enabled == 1),
|
||||
worlds: book_tools.as_ref().map_or(false, |book_tools_row| book_tools_row.worlds_enabled == 1),
|
||||
locations: book_tools.as_ref().map_or(false, |book_tools_row| book_tools_row.locations_enabled == 1),
|
||||
spells: book_tools.as_ref().map_or(false, |book_tools_row| book_tools_row.spells_enabled == 1),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// Updates basic information for a book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `title` - The new title
|
||||
/// * `sub_title` - The new subtitle
|
||||
/// * `summary` - The new summary
|
||||
/// * `publication_date` - The new publication date
|
||||
/// * `word_count` - The new word count
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the update was successful.
|
||||
pub fn update_book_basic_information(
|
||||
conn: &Connection, user_id: &str, title: &str, sub_title: &str, summary: &str,
|
||||
publication_date: Option<&str>, word_count: i64, book_id: &str, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let encrypted_title: String = encrypt_data_with_user_key(title, &user_key)?;
|
||||
let encrypted_sub_title: String = if sub_title.is_empty() { String::new() } else { encrypt_data_with_user_key(sub_title, &user_key)? };
|
||||
let encrypted_summary: String = if summary.is_empty() { String::new() } else { encrypt_data_with_user_key(summary, &user_key)? };
|
||||
let hashed_title: String = hash_element(title);
|
||||
let hashed_sub_title: String = if sub_title.is_empty() { String::new() } else { hash_element(sub_title) };
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
repo::update_book_basic_information(conn, user_id, &encrypted_title, &hashed_title, &encrypted_sub_title, &hashed_sub_title, &encrypted_summary, publication_date, word_count, book_id, last_update, lang)
|
||||
}
|
||||
|
||||
/// Removes a book from the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book to remove
|
||||
/// * `deleted_at` - The timestamp of deletion
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the book was removed.
|
||||
pub fn remove_book(conn: &Connection, user_id: &str, book_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
|
||||
let deleted: bool = repo::delete_book(conn, user_id, book_id, lang)?;
|
||||
if deleted {
|
||||
tombstone_repo::insert(conn, book_id, "erit_books", book_id, None, user_id, deleted_at, lang)?;
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
/// Updates a book tool setting (characters, worlds, locations, or spells).
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `tool_name` - The tool name ("characters", "worlds", "locations", "spells")
|
||||
/// * `enabled` - Whether the tool is enabled
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the update was successful.
|
||||
pub fn update_book_tool_setting(conn: &Connection, user_id: &str, book_id: &str, tool_name: &str, enabled: bool, lang: Lang) -> AppResult<bool> {
|
||||
let column_name: String = format!("{}_enabled", tool_name);
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
repo::update_book_tool_setting(conn, user_id, book_id, &column_name, enabled, last_update, lang)
|
||||
}
|
||||
|
||||
/// Retrieves complete book data including chapters and user information.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the complete book data with decrypted content.
|
||||
pub fn complete_book_data(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<CompleteBookData> {
|
||||
let book_data: repo::BookQuery = repo::fetch_book(conn, book_id, user_id, lang)?;
|
||||
let chapters: Vec<chapter_repo::ChapterBookResult> = chapter_repo::fetch_complete_book_chapters(conn, book_id, lang)?;
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let user_infos: user_repo::UserAccountQuery = user_repo::fetch_account_information(conn, user_id, lang)?;
|
||||
|
||||
let book_title: String = decrypt_data_with_user_key(&book_data.title, &user_key)?;
|
||||
let mut decrypted_chapters: Vec<CompleteChapterContent> = Vec::with_capacity(chapters.len());
|
||||
|
||||
for chapter in chapters {
|
||||
let decrypted_title: String = if chapter.title.is_empty() { String::new() } else { decrypt_data_with_user_key(&chapter.title, &user_key)? };
|
||||
let decrypted_content: String = if let Some(ref content) = chapter.content { decrypt_data_with_user_key(content, &user_key)? } else { String::new() };
|
||||
decrypted_chapters.push(CompleteChapterContent {
|
||||
id: String::new(),
|
||||
title: decrypted_title,
|
||||
content: decrypted_content,
|
||||
order: chapter.chapter_order,
|
||||
version: None,
|
||||
});
|
||||
}
|
||||
|
||||
let cover_image: String = book_data.cover_image.unwrap_or_default();
|
||||
|
||||
Ok(CompleteBookData {
|
||||
book_id: book_id.to_string(),
|
||||
title: book_title,
|
||||
sub_title: if let Some(ref sub_title) = book_data.sub_title { decrypt_data_with_user_key(sub_title, &user_key)? } else { String::new() },
|
||||
summary: if let Some(ref summary) = book_data.summary { decrypt_data_with_user_key(summary, &user_key)? } else { String::new() },
|
||||
cover_image,
|
||||
user_infos: BookUserInfos {
|
||||
first_name: if let Some(ref first_name) = user_infos.first_name { decrypt_data_with_user_key(first_name, &user_key)? } else { String::new() },
|
||||
last_name: if let Some(ref last_name) = user_infos.last_name { decrypt_data_with_user_key(last_name, &user_key)? } else { String::new() },
|
||||
author_name: if let Some(ref author_name) = user_infos.author_name { decrypt_data_with_user_key(author_name, &user_key)? } else { String::new() },
|
||||
},
|
||||
chapters: decrypted_chapters,
|
||||
})
|
||||
}
|
||||
239
src-tauri/src/domains/chapter/commands.rs
Normal file
239
src-tauri/src/domains/chapter/commands.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tauri::State;
|
||||
|
||||
use crate::db::connection::DbManager;
|
||||
use crate::domains::chapter::service;
|
||||
use crate::error::AppError;
|
||||
use crate::helpers::timestamp_in_seconds;
|
||||
use crate::shared::session::SessionState;
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
fn get_session(session: &State<SessionState>) -> Result<(String, Lang), AppError> {
|
||||
let session_guard = session.lock().map_err(|e| AppError::Internal(format!("Session lock failed: {}", e)))?;
|
||||
let user_id = session_guard.get_user_id().map_err(|e| AppError::Auth(e))?.to_string();
|
||||
let lang = session_guard.lang;
|
||||
Ok((user_id, lang))
|
||||
}
|
||||
|
||||
// ─── Queries ──────────────────────────────────────────
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_chapters(book_id: String, db: State<DbManager>, session: State<SessionState>) -> Result<Vec<service::ChapterProps>, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_all_chapters_from_a_book(conn, &user_id, &book_id, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetWholeChapterData {
|
||||
pub id: String,
|
||||
pub version: i64,
|
||||
pub book_id: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_whole_chapter(data: GetWholeChapterData, db: State<DbManager>, session: State<SessionState>) -> Result<service::ChapterProps, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_whole_chapter(conn, &user_id, &data.id, data.version, Some(&data.book_id), lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_chapter_story(chapter_id: String, db: State<DbManager>, session: State<SessionState>) -> Result<Vec<service::ActStory>, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_chapter_story(conn, &user_id, &chapter_id, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCompanionData {
|
||||
pub chapter_id: String,
|
||||
pub version: i64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_companion_content(data: GetCompanionData, db: State<DbManager>, session: State<SessionState>) -> Result<service::CompanionContent, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_companion_content(conn, &user_id, &data.chapter_id, data.version, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetChapterContentData {
|
||||
pub chapter_id: String,
|
||||
pub version: i64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_chapter_content(data: GetChapterContentData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_chapter_content_by_version(conn, &user_id, &data.chapter_id, data.version, lang)
|
||||
}
|
||||
|
||||
// ─── Mutations ────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SaveChapterContentData {
|
||||
pub chapter_id: String,
|
||||
pub version: i64,
|
||||
pub content: Value,
|
||||
pub total_word_count: i64,
|
||||
pub content_id: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn save_chapter_content(data: SaveChapterContentData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
let content_str = serde_json::to_string(&data.content).map_err(|e| AppError::Internal(format!("JSON serialize failed: {}", e)))?;
|
||||
service::save_chapter_content(conn, &user_id, &data.chapter_id, data.version, &content_str, data.total_word_count, timestamp_in_seconds(), lang)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_last_chapter(book_id: String, db: State<DbManager>, session: State<SessionState>) -> Result<Option<service::ChapterProps>, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_last_chapter(conn, &user_id, &book_id, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddChapterData {
|
||||
pub book_id: String,
|
||||
pub title: String,
|
||||
pub chapter_order: i64,
|
||||
pub chapter_id: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_chapter(data: AddChapterData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::add_chapter(conn, &user_id, &data.book_id, &data.title, 0, data.chapter_order, lang, data.chapter_id.as_deref())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveChapterData {
|
||||
pub chapter_id: String,
|
||||
pub book_id: String,
|
||||
pub deleted_at: i64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_chapter(data: RemoveChapterData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::remove_chapter(conn, &user_id, &data.book_id, &data.chapter_id, data.deleted_at, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateChapterData {
|
||||
pub chapter_id: String,
|
||||
pub title: String,
|
||||
pub chapter_order: i64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_chapter(data: UpdateChapterData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::update_chapter(conn, &user_id, &data.chapter_id, &data.title, data.chapter_order, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddChapterInfoData {
|
||||
pub chapter_id: String,
|
||||
pub act_id: i64,
|
||||
pub book_id: String,
|
||||
pub plot_id: Option<String>,
|
||||
pub incident_id: Option<String>,
|
||||
pub chapter_info_id: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_chapter_information(data: AddChapterInfoData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::add_chapter_information(conn, &user_id, &data.chapter_id, data.act_id, &data.book_id, data.plot_id.as_deref(), data.incident_id.as_deref(), lang, data.chapter_info_id.as_deref())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveChapterInfoData {
|
||||
pub chapter_info_id: String,
|
||||
pub book_id: String,
|
||||
pub deleted_at: i64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_chapter_information(data: RemoveChapterInfoData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::remove_chapter_information(conn, &user_id, &data.book_id, &data.chapter_info_id, data.deleted_at, lang)
|
||||
}
|
||||
|
||||
// ─── Book Tags (aggregate) ───────────────────────────
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Tag {
|
||||
pub label: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BookTags {
|
||||
pub characters: Vec<Tag>,
|
||||
pub locations: Vec<Tag>,
|
||||
pub objects: Vec<Tag>,
|
||||
pub world_elements: Vec<Tag>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_book_tags(book_id: String, db: State<DbManager>, session: State<SessionState>) -> Result<BookTags, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
|
||||
let character_response = crate::domains::character::service::get_character_list(conn, &user_id, &book_id, lang)?;
|
||||
let character_tags: Vec<Tag> = character_response.characters.into_iter().map(|character| Tag { label: character.name, value: character.id }).collect();
|
||||
|
||||
let location_elements = crate::domains::location::service::get_location_tags(conn, &user_id, &book_id, lang)?;
|
||||
let location_tags: Vec<Tag> = location_elements.into_iter().map(|element| Tag { label: element.name, value: element.id }).collect();
|
||||
|
||||
let spell_response = crate::domains::spell::service::get_spell_list(conn, &user_id, &book_id, lang)?;
|
||||
let object_tags: Vec<Tag> = spell_response.spells.into_iter().map(|spell| Tag { label: spell.name, value: spell.id }).collect();
|
||||
|
||||
let world_response = crate::domains::world::service::get_worlds(conn, &user_id, &book_id, lang)?;
|
||||
let mut world_tags: Vec<Tag> = Vec::new();
|
||||
for world in &world_response.worlds {
|
||||
for element_list in [&world.laws, &world.biomes, &world.issues, &world.customs, &world.kingdoms, &world.climate, &world.resources, &world.wildlife, &world.arts, &world.ethnic_groups, &world.social_classes, &world.important_characters] {
|
||||
for element in element_list {
|
||||
world_tags.push(Tag { label: element.name.clone(), value: element.id.clone() });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(BookTags { characters: character_tags, locations: location_tags, objects: object_tags, world_elements: world_tags })
|
||||
}
|
||||
3
src-tauri/src/domains/chapter/mod.rs
Normal file
3
src-tauri/src/domains/chapter/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod commands;
|
||||
pub mod repo;
|
||||
pub mod service;
|
||||
592
src-tauri/src/domains/chapter/repo.rs
Normal file
592
src-tauri/src/domains/chapter/repo.rs
Normal file
@@ -0,0 +1,592 @@
|
||||
use rusqlite::{params, Connection};
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::helpers::timestamp_in_seconds;
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
pub struct ChapterQueryResult {
|
||||
pub chapter_id: String,
|
||||
pub title: String,
|
||||
pub chapter_order: i64,
|
||||
}
|
||||
|
||||
pub struct ActChapterQuery {
|
||||
pub chapter_info_id: i64,
|
||||
pub chapter_id: String,
|
||||
pub title: String,
|
||||
pub chapter_order: i64,
|
||||
pub act_id: i64,
|
||||
pub incident_id: Option<String>,
|
||||
pub plot_point_id: Option<String>,
|
||||
pub summary: String,
|
||||
pub goal: String,
|
||||
}
|
||||
|
||||
pub struct ChapterStoryQueryResult {
|
||||
pub chapter_info_id: i64,
|
||||
pub act_id: i64,
|
||||
pub summary: String,
|
||||
pub chapter_summary: String,
|
||||
pub chapter_goal: String,
|
||||
pub incident_id: Option<i64>,
|
||||
pub incident_title: Option<String>,
|
||||
pub incident_summary: Option<String>,
|
||||
pub plot_point_id: Option<i64>,
|
||||
pub plot_title: Option<String>,
|
||||
pub plot_summary: Option<String>,
|
||||
}
|
||||
|
||||
pub struct LastChapterResult {
|
||||
pub chapter_id: String,
|
||||
pub version: i64,
|
||||
}
|
||||
|
||||
pub struct BookChaptersTable {
|
||||
pub chapter_id: String,
|
||||
pub book_id: String,
|
||||
pub author_id: String,
|
||||
pub title: String,
|
||||
pub hashed_title: String,
|
||||
pub words_count: Option<i64>,
|
||||
pub chapter_order: i64,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct BookChapterInfosTable {
|
||||
pub chapter_info_id: String,
|
||||
pub chapter_id: String,
|
||||
pub act_id: Option<i64>,
|
||||
pub incident_id: Option<String>,
|
||||
pub plot_point_id: Option<String>,
|
||||
pub book_id: String,
|
||||
pub author_id: String,
|
||||
pub summary: Option<String>,
|
||||
pub goal: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedChapterResult {
|
||||
pub chapter_id: String,
|
||||
pub book_id: String,
|
||||
pub title: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedChapterInfoResult {
|
||||
pub chapter_info_id: String,
|
||||
pub chapter_id: Option<String>,
|
||||
pub book_id: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct ChapterBookResult {
|
||||
pub title: String,
|
||||
pub chapter_order: i64,
|
||||
pub content: Option<String>,
|
||||
}
|
||||
|
||||
pub struct ChapterExportInfoResult {
|
||||
pub chapter_id: String,
|
||||
pub title: String,
|
||||
pub chapter_order: i64,
|
||||
pub available_versions: String,
|
||||
}
|
||||
|
||||
pub struct SelectedChapterContentResult {
|
||||
pub chapter_id: String,
|
||||
pub title: String,
|
||||
pub chapter_order: i64,
|
||||
pub content: String,
|
||||
pub version: i64,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChapterSelectionParam {
|
||||
pub chapter_id: String,
|
||||
pub version: i64,
|
||||
}
|
||||
|
||||
/// Checks if a chapter name already exists for a book.
|
||||
pub fn check_name_duplication(conn: &Connection, user_id: &str, book_id: &str, hashed_title: &str, lang: Lang) -> AppResult<bool> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT chapter_id FROM book_chapters WHERE author_id=?1 AND book_id=?2 AND hashed_title=?3")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier la duplication du nom.".to_string() } else { "Unable to verify name duplication.".to_string() }))?;
|
||||
|
||||
let exists = statement
|
||||
.exists(params![user_id, book_id, hashed_title])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier la duplication du nom.".to_string() } else { "Unable to verify name duplication.".to_string() }))?;
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Inserts a new chapter into the database.
|
||||
pub fn insert_chapter(conn: &Connection, chapter_id: &str, user_id: &str, book_id: &str, title: &str, hashed_title: &str, words_count: i64, chapter_order: i64, lang: Lang) -> AppResult<String> {
|
||||
let insert_result = conn
|
||||
.execute("INSERT INTO book_chapters (chapter_id, author_id, book_id, title, hashed_title, words_count, chapter_order, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8)", params![chapter_id, user_id, book_id, title, hashed_title, words_count, chapter_order, timestamp_in_seconds()])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le chapitre.".to_string() } else { "Unable to add chapter.".to_string() }))?;
|
||||
|
||||
if insert_result > 0 {
|
||||
Ok(chapter_id.to_string())
|
||||
} else {
|
||||
Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est passé lors de l'ajout du chapitre.".to_string() } else { "Error adding chapter.".to_string() }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves all chapters with their act information for a book.
|
||||
pub fn fetch_all_chapter_for_acts(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<ActChapterQuery>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT ci.chapter_info_id AS chapter_info_id, ci.chapter_id AS chapter_id, chapter.title, chapter.chapter_order, ci.act_id, ci.incident_id AS incident_id, ci.plot_point_id AS plot_point_id, ci.summary, ci.goal FROM book_chapter_infos AS ci INNER JOIN book_chapters AS chapter ON chapter.chapter_id = ci.chapter_id WHERE ci.book_id = ?1 AND ci.author_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les chapitres pour les actes.".to_string() } else { "Unable to retrieve chapters for acts.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![book_id, user_id], |query_row| {
|
||||
Ok(ActChapterQuery {
|
||||
chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
|
||||
title: query_row.get(2)?, chapter_order: query_row.get(3)?,
|
||||
act_id: query_row.get(4)?, incident_id: query_row.get(5)?,
|
||||
plot_point_id: query_row.get(6)?, summary: query_row.get(7)?,
|
||||
goal: query_row.get(8)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les chapitres pour les actes.".to_string() } else { "Unable to retrieve chapters for acts.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les chapitres pour les actes.".to_string() } else { "Unable to retrieve chapters for acts.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Retrieves all chapters from a book ordered by chapter order.
|
||||
pub fn fetch_all_chapter_from_a_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<ChapterQueryResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT chapter_id, title, chapter_order FROM book_chapters WHERE book_id=?1 AND author_id=?2 ORDER BY chapter_order")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les chapitres.".to_string() } else { "Unable to retrieve chapters.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![book_id, user_id], |query_row| {
|
||||
Ok(ChapterQueryResult {
|
||||
chapter_id: query_row.get(0)?, title: query_row.get(1)?,
|
||||
chapter_order: query_row.get(2)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les chapitres.".to_string() } else { "Unable to retrieve chapters.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les chapitres.".to_string() } else { "Unable to retrieve chapters.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Deletes a chapter from the database.
|
||||
pub fn delete_chapter(conn: &Connection, user_id: &str, chapter_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let delete_result = conn
|
||||
.execute("DELETE FROM book_chapters WHERE author_id=?1 AND chapter_id=?2", params![user_id, chapter_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le chapitre.".to_string() } else { "Unable to delete chapter.".to_string() }))?;
|
||||
|
||||
Ok(delete_result > 0)
|
||||
}
|
||||
|
||||
/// Inserts chapter information linking a chapter to an act.
|
||||
pub fn insert_chapter_information(
|
||||
conn: &Connection, chapter_info_id: &str, user_id: &str, chapter_id: &str,
|
||||
act_id: i64, book_id: &str, plot_id: Option<&str>, incident_id: Option<&str>, lang: Lang,
|
||||
) -> AppResult<String> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT chapter_info_id FROM book_chapter_infos WHERE chapter_id=?1 AND act_id=?2 AND book_id=?3 AND plot_point_id=?4 AND incident_id=?5 AND author_id=?6")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'information du chapitre.".to_string() } else { "Unable to verify chapter information existence.".to_string() }))?;
|
||||
|
||||
let existing = statement
|
||||
.exists(params![chapter_id, act_id, book_id, plot_id, incident_id, user_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'information du chapitre.".to_string() } else { "Unable to verify chapter information existence.".to_string() }))?;
|
||||
|
||||
if existing {
|
||||
return Err(AppError::Internal(if lang == Lang::Fr { "Le chapitre est déjà lié.".to_string() } else { "Chapter is already linked.".to_string() }));
|
||||
}
|
||||
|
||||
let insert_result = conn
|
||||
.execute("INSERT INTO book_chapter_infos (chapter_info_id, chapter_id, act_id, book_id, author_id, incident_id, plot_point_id, summary, goal, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10)", params![chapter_info_id, chapter_id, act_id, book_id, user_id, incident_id, plot_id, "", "", timestamp_in_seconds()])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter l'information du chapitre.".to_string() } else { "Unable to add chapter information.".to_string() }))?;
|
||||
|
||||
if insert_result > 0 {
|
||||
Ok(chapter_info_id.to_string())
|
||||
} else {
|
||||
Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite pendant la liaison du chapitre.".to_string() } else { "Error linking chapter.".to_string() }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates a chapter's basic information.
|
||||
pub fn update_chapter(conn: &Connection, user_id: &str, chapter_id: &str, encrypted_title: &str, hash_title: &str, chapter_order: i64, last_update: i64, lang: Lang) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute("UPDATE book_chapters SET title=?1, hashed_title=?2, chapter_order=?3, last_update=?4 WHERE author_id=?5 AND chapter_id=?6", params![encrypted_title, hash_title, chapter_order, last_update, user_id, chapter_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le chapitre.".to_string() } else { "Unable to update chapter.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Updates chapter information (summary and goal).
|
||||
pub fn update_chapter_infos(
|
||||
conn: &Connection, user_id: &str, chapter_id: &str, act_id: i64, book_id: &str,
|
||||
incident_id: Option<&str>, plot_id: Option<&str>, summary: &str, goal: Option<&str>,
|
||||
last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let mut query = "UPDATE book_chapter_infos SET summary=?1,goal=?2,last_update=?3 WHERE chapter_id = ?4 AND act_id = ?5 AND book_id = ?6".to_string();
|
||||
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = vec![
|
||||
Box::new(summary.to_string()), Box::new(goal.map(|g| g.to_string())),
|
||||
Box::new(last_update), Box::new(chapter_id.to_string()),
|
||||
Box::new(act_id), Box::new(book_id.to_string()),
|
||||
];
|
||||
|
||||
if let Some(incident) = incident_id {
|
||||
let idx = param_values.len() + 1;
|
||||
query.push_str(&format!(" AND incident_id=?{}", idx));
|
||||
param_values.push(Box::new(incident.to_string()));
|
||||
} else {
|
||||
query.push_str(" AND incident_id IS NULL");
|
||||
}
|
||||
|
||||
if let Some(plot) = plot_id {
|
||||
let idx = param_values.len() + 1;
|
||||
query.push_str(&format!(" AND plot_point_id=?{}", idx));
|
||||
param_values.push(Box::new(plot.to_string()));
|
||||
} else {
|
||||
query.push_str(" AND plot_point_id IS NULL");
|
||||
}
|
||||
|
||||
let idx = param_values.len() + 1;
|
||||
query.push_str(&format!(" AND author_id=?{}", idx));
|
||||
param_values.push(Box::new(user_id.to_string()));
|
||||
|
||||
let params_ref: Vec<&dyn rusqlite::types::ToSql> = param_values.iter().map(|p| p.as_ref()).collect();
|
||||
let update_result = conn
|
||||
.execute(&query, params_ref.as_slice())
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour les informations du chapitre.".to_string() } else { "Unable to update chapter information.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Retrieves the last opened chapter for a book.
|
||||
pub fn fetch_last_chapter(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Option<LastChapterResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT chapter_id as chapter_id,version FROM user_last_chapter WHERE user_id=?1 AND book_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le dernier chapitre ouvert.".to_string() } else { "Unable to retrieve last opened chapter.".to_string() }))?;
|
||||
|
||||
let result = statement
|
||||
.query_row(params![user_id, book_id], |query_row| {
|
||||
Ok(LastChapterResult { chapter_id: query_row.get(0)?, version: query_row.get(1)? })
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(row) => Ok(Some(row)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le dernier chapitre ouvert.".to_string() } else { "Unable to retrieve last opened chapter.".to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates or inserts the last chapter record for a user.
|
||||
pub fn update_last_chapter_record(conn: &Connection, user_id: &str, book_id: &str, chapter_id: &str, version: i64, lang: Lang) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute("UPDATE user_last_chapter SET chapter_id=?1, version=?2 WHERE user_id=?3 AND book_id=?4", params![chapter_id, version, user_id, book_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'enregistrer le dernier chapitre.".to_string() } else { "Unable to save last chapter.".to_string() }))?;
|
||||
|
||||
if update_result > 0 {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let insert_result = conn
|
||||
.execute("INSERT INTO user_last_chapter (user_id, book_id, chapter_id, version) VALUES (?1,?2,?3,?4)", params![user_id, book_id, chapter_id, version])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'enregistrer le dernier chapitre.".to_string() } else { "Unable to save last chapter.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Retrieves chapter story information including act, incident, and plot point data.
|
||||
pub fn fetch_chapter_story(conn: &Connection, user_id: &str, chapter_id: &str, lang: Lang) -> AppResult<Vec<ChapterStoryQueryResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT chapter_info_id, chapter.act_id, act_sum.summary, chapter.summary AS chapter_summary, chapter.goal AS chapter_goal, chapter.incident_id, incident.title AS incident_title, incident.summary AS incident_summary, chapter.plot_point_id, plot.title AS plot_title, plot.summary AS plot_summary FROM book_chapter_infos AS chapter LEFT JOIN book_incidents AS incident ON chapter.incident_id=incident.incident_id LEFT JOIN book_plot_points AS plot ON chapter.plot_point_id=plot.plot_point_id LEFT JOIN book_act_summaries AS act_sum ON chapter.act_id=act_sum.act_sum_id AND chapter.book_id=act_sum.book_id WHERE chapter.chapter_id=?1 AND chapter.author_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'histoire du chapitre.".to_string() } else { "Unable to retrieve chapter story.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![chapter_id, user_id], |query_row| {
|
||||
Ok(ChapterStoryQueryResult {
|
||||
chapter_info_id: query_row.get(0)?, act_id: query_row.get(1)?,
|
||||
summary: query_row.get::<_, Option<String>>(2)?.unwrap_or_default(),
|
||||
chapter_summary: query_row.get::<_, Option<String>>(3)?.unwrap_or_default(),
|
||||
chapter_goal: query_row.get::<_, Option<String>>(4)?.unwrap_or_default(),
|
||||
incident_id: query_row.get(5)?, incident_title: query_row.get(6)?,
|
||||
incident_summary: query_row.get(7)?, plot_point_id: query_row.get(8)?,
|
||||
plot_title: query_row.get(9)?, plot_summary: query_row.get(10)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'histoire du chapitre.".to_string() } else { "Unable to retrieve chapter story.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'histoire du chapitre.".to_string() } else { "Unable to retrieve chapter story.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Deletes chapter information by its identifier.
|
||||
pub fn delete_chapter_information(conn: &Connection, user_id: &str, chapter_info_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let delete_result = conn
|
||||
.execute("DELETE FROM book_chapter_infos WHERE chapter_info_id=?1 AND author_id=?2", params![chapter_info_id, user_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer les informations du chapitre.".to_string() } else { "Unable to delete chapter information.".to_string() }))?;
|
||||
|
||||
Ok(delete_result > 0)
|
||||
}
|
||||
|
||||
/// Checks if a chapter exists.
|
||||
pub fn is_chapter_exist(conn: &Connection, user_id: &str, chapter_id: &str, _lang: Lang) -> bool {
|
||||
let result = conn.prepare("SELECT 1 FROM book_chapters WHERE chapter_id=?1 AND author_id=?2");
|
||||
|
||||
match result {
|
||||
Ok(mut statement) => statement.exists(params![chapter_id, user_id]).unwrap_or(false),
|
||||
Err(error) => {
|
||||
eprintln!("DB Error: {}", error);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if chapter info exists.
|
||||
pub fn is_chapter_info_exist(conn: &Connection, user_id: &str, chapter_id: &str, _lang: Lang) -> bool {
|
||||
let result = conn.prepare("SELECT 1 FROM book_chapter_infos WHERE chapter_id=?1 AND author_id=?2");
|
||||
|
||||
match result {
|
||||
Ok(mut statement) => statement.exists(params![chapter_id, user_id]).unwrap_or(false),
|
||||
Err(error) => {
|
||||
eprintln!("DB Error: {}", error);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves complete book chapters with their content.
|
||||
pub fn fetch_complete_book_chapters(conn: &Connection, book_id: &str, lang: Lang) -> AppResult<Vec<ChapterBookResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT title, chapter_order, content.content FROM book_chapters AS chapter LEFT JOIN book_chapter_content AS content ON chapter.chapter_id = content.chapter_id AND content.version = (SELECT MAX(version) FROM book_chapter_content WHERE chapter_id = chapter.chapter_id AND version > 1) WHERE chapter.book_id = ?1 ORDER BY chapter.chapter_order")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les chapitres.".to_string() } else { "Unable to retrieve chapters.".to_string() }))?;
|
||||
|
||||
let chapters = statement
|
||||
.query_map(params![book_id], |query_row| {
|
||||
Ok(ChapterBookResult {
|
||||
title: query_row.get(0)?, chapter_order: query_row.get(1)?,
|
||||
content: query_row.get(2)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les chapitres.".to_string() } else { "Unable to retrieve chapters.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les chapitres.".to_string() } else { "Unable to retrieve chapters.".to_string() }))?;
|
||||
|
||||
if chapters.is_empty() {
|
||||
return Err(AppError::NotFound(if lang == Lang::Fr { "Aucun chapitre trouvé.".to_string() } else { "No chapters found.".to_string() }));
|
||||
}
|
||||
|
||||
Ok(chapters)
|
||||
}
|
||||
|
||||
/// Retrieves all chapters for a book.
|
||||
pub fn fetch_book_chapters(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookChaptersTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT chapter_id, book_id, author_id, title, hashed_title, words_count, chapter_order, last_update FROM book_chapters WHERE author_id=?1 AND book_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les chapitres.".to_string() } else { "Unable to retrieve chapters.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![user_id, book_id], |query_row| {
|
||||
Ok(BookChaptersTable {
|
||||
chapter_id: query_row.get(0)?, book_id: query_row.get(1)?,
|
||||
author_id: query_row.get(2)?, title: query_row.get(3)?,
|
||||
hashed_title: query_row.get(4)?, words_count: query_row.get(5)?,
|
||||
chapter_order: query_row.get(6)?, last_update: query_row.get(7)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les chapitres.".to_string() } else { "Unable to retrieve chapters.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les chapitres.".to_string() } else { "Unable to retrieve chapters.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Retrieves chapter information for a specific chapter.
|
||||
pub fn fetch_book_chapter_infos(conn: &Connection, user_id: &str, chapter_id: &str, lang: Lang) -> AppResult<Vec<BookChapterInfosTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update FROM book_chapter_infos WHERE author_id=?1 AND chapter_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres.".to_string() } else { "Unable to retrieve chapter infos.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![user_id, chapter_id], |query_row| {
|
||||
Ok(BookChapterInfosTable {
|
||||
chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
|
||||
act_id: query_row.get(2)?, incident_id: query_row.get(3)?,
|
||||
plot_point_id: query_row.get(4)?, book_id: query_row.get(5)?,
|
||||
author_id: query_row.get(6)?, summary: query_row.get(7)?,
|
||||
goal: query_row.get(8)?, last_update: query_row.get(9)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres.".to_string() } else { "Unable to retrieve chapter infos.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres.".to_string() } else { "Unable to retrieve chapter infos.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Retrieves synced chapters for a user.
|
||||
pub fn fetch_synced_chapters(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedChapterResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT chapter_id, book_id, title, last_update FROM book_chapters WHERE author_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les chapitres synchronisés.".to_string() } else { "Unable to retrieve synced chapters.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedChapterResult {
|
||||
chapter_id: query_row.get(0)?, book_id: query_row.get(1)?,
|
||||
title: query_row.get(2)?, last_update: query_row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les chapitres synchronisés.".to_string() } else { "Unable to retrieve synced chapters.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les chapitres synchronisés.".to_string() } else { "Unable to retrieve synced chapters.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Retrieves synced chapter infos for a user.
|
||||
pub fn fetch_synced_chapter_infos(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedChapterInfoResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT chapter_info_id, chapter_id, book_id, last_update FROM book_chapter_infos WHERE author_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres synchronisés.".to_string() } else { "Unable to retrieve synced chapter infos.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedChapterInfoResult {
|
||||
chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
|
||||
book_id: query_row.get(2)?, last_update: query_row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres synchronisés.".to_string() } else { "Unable to retrieve synced chapter infos.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les infos des chapitres synchronisés.".to_string() } else { "Unable to retrieve synced chapter infos.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Inserts a synced chapter from the server.
|
||||
pub fn insert_sync_chapter(conn: &Connection, chapter_id: &str, book_id: &str, author_id: &str, title: &str, hashed_title: Option<&str>, words_count: Option<i64>, chapter_order: Option<i64>, last_update: i64, lang: Lang) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute("INSERT INTO book_chapters (chapter_id, book_id, author_id, title, hashed_title, words_count, chapter_order, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", params![chapter_id, book_id, author_id, title, hashed_title, words_count, chapter_order, last_update])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le chapitre.".to_string() } else { "Unable to insert chapter.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Inserts synced chapter info from the server.
|
||||
pub fn insert_sync_chapter_info(
|
||||
conn: &Connection, chapter_info_id: &str, chapter_id: &str, act_id: Option<i64>,
|
||||
incident_id: Option<&str>, plot_point_id: Option<&str>, book_id: &str, author_id: &str,
|
||||
summary: Option<&str>, goal: Option<&str>, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute("INSERT INTO book_chapter_infos (chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)", params![chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer les infos du chapitre.".to_string() } else { "Unable to insert chapter info.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Retrieves a complete chapter by its identifier.
|
||||
pub fn fetch_complete_chapter_by_id(conn: &Connection, chapter_id: &str, lang: Lang) -> AppResult<Vec<BookChaptersTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT chapter_id, book_id, author_id, title, hashed_title, words_count, chapter_order, last_update FROM book_chapters WHERE chapter_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le chapitre complet.".to_string() } else { "Unable to retrieve complete chapter.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![chapter_id], |query_row| {
|
||||
Ok(BookChaptersTable {
|
||||
chapter_id: query_row.get(0)?, book_id: query_row.get(1)?,
|
||||
author_id: query_row.get(2)?, title: query_row.get(3)?,
|
||||
hashed_title: query_row.get(4)?, words_count: query_row.get(5)?,
|
||||
chapter_order: query_row.get(6)?, last_update: query_row.get(7)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le chapitre complet.".to_string() } else { "Unable to retrieve complete chapter.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le chapitre complet.".to_string() } else { "Unable to retrieve complete chapter.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Retrieves complete chapter info by its identifier.
|
||||
pub fn fetch_complete_chapter_info_by_id(conn: &Connection, chapter_info_id: &str, lang: Lang) -> AppResult<Vec<BookChapterInfosTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT chapter_info_id, chapter_id, act_id, incident_id, plot_point_id, book_id, author_id, summary, goal, last_update FROM book_chapter_infos WHERE chapter_info_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les informations de chapitre complètes.".to_string() } else { "Unable to retrieve complete chapter info.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![chapter_info_id], |query_row| {
|
||||
Ok(BookChapterInfosTable {
|
||||
chapter_info_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
|
||||
act_id: query_row.get(2)?, incident_id: query_row.get(3)?,
|
||||
plot_point_id: query_row.get(4)?, book_id: query_row.get(5)?,
|
||||
author_id: query_row.get(6)?, summary: query_row.get(7)?,
|
||||
goal: query_row.get(8)?, last_update: query_row.get(9)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les informations de chapitre complètes.".to_string() } else { "Unable to retrieve complete chapter info.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les informations de chapitre complètes.".to_string() } else { "Unable to retrieve complete chapter info.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Retrieves chapter export information for a book.
|
||||
pub fn fetch_chapters_export_info(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<ChapterExportInfoResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT bc.chapter_id, bc.title, bc.chapter_order, GROUP_CONCAT(DISTINCT bcc.version) AS available_versions FROM book_chapters bc LEFT JOIN book_chapter_content bcc ON bc.chapter_id = bcc.chapter_id WHERE bc.author_id = ?1 AND bc.book_id = ?2 GROUP BY bc.chapter_id, bc.title, bc.chapter_order ORDER BY bc.chapter_order")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les informations d'export des chapitres.".to_string() } else { "Unable to retrieve chapters export info.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![user_id, book_id], |query_row| {
|
||||
Ok(ChapterExportInfoResult {
|
||||
chapter_id: query_row.get(0)?, title: query_row.get(1)?,
|
||||
chapter_order: query_row.get(2)?,
|
||||
available_versions: query_row.get::<_, Option<String>>(3)?.unwrap_or_default(),
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les informations d'export des chapitres.".to_string() } else { "Unable to retrieve chapters export info.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les informations d'export des chapitres.".to_string() } else { "Unable to retrieve chapters export info.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Retrieves content for selected chapters with specific versions.
|
||||
pub fn fetch_selected_chapters_content(conn: &Connection, book_id: &str, selections: &[ChapterSelectionParam], lang: Lang) -> AppResult<Vec<SelectedChapterContentResult>> {
|
||||
let conditions: Vec<String> = selections.iter().enumerate().map(|(index, _)| {
|
||||
let base = 2 + index * 2;
|
||||
format!("(chapter.chapter_id = ?{} AND content.version = ?{})", base, base + 1)
|
||||
}).collect();
|
||||
|
||||
let query = format!("SELECT chapter.chapter_id, chapter.title, chapter.chapter_order, content.content, content.version FROM book_chapters AS chapter INNER JOIN book_chapter_content AS content ON chapter.chapter_id = content.chapter_id WHERE chapter.book_id = ?1 AND ({}) ORDER BY chapter.chapter_order", conditions.join(" OR "));
|
||||
|
||||
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = vec![Box::new(book_id.to_string())];
|
||||
for selection in selections {
|
||||
param_values.push(Box::new(selection.chapter_id.clone()));
|
||||
param_values.push(Box::new(selection.version));
|
||||
}
|
||||
|
||||
let params_ref: Vec<&dyn rusqlite::types::ToSql> = param_values.iter().map(|p| p.as_ref()).collect();
|
||||
|
||||
let mut statement = conn
|
||||
.prepare(&query)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres sélectionnés.".to_string() } else { "Unable to retrieve selected chapters content.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params_ref.as_slice(), |query_row| {
|
||||
Ok(SelectedChapterContentResult {
|
||||
chapter_id: query_row.get(0)?, title: query_row.get(1)?,
|
||||
chapter_order: query_row.get(2)?, content: query_row.get(3)?,
|
||||
version: query_row.get(4)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres sélectionnés.".to_string() } else { "Unable to retrieve selected chapters content.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres sélectionnés.".to_string() } else { "Unable to retrieve selected chapters content.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
692
src-tauri/src/domains/chapter/service.rs
Normal file
692
src-tauri/src/domains/chapter/service.rs
Normal file
@@ -0,0 +1,692 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::crypto::encryption::{decrypt_data_with_user_key, encrypt_data_with_user_key, hash_element};
|
||||
use crate::crypto::key_manager::get_user_encryption_key;
|
||||
use crate::domains::book::service as book_service;
|
||||
use crate::domains::chapter::repo;
|
||||
use crate::domains::chapter_content::repo as chapter_content_repo;
|
||||
use crate::domains::tombstone::repo as tombstone_repo;
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::helpers::{create_unique_id, html_to_text, timestamp_in_seconds};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChapterContent {
|
||||
pub version: i64,
|
||||
pub content: String,
|
||||
pub words_count: i64,
|
||||
}
|
||||
|
||||
pub struct ChapterContentData {
|
||||
pub title: String,
|
||||
pub chapter_order: i64,
|
||||
pub content: String,
|
||||
pub words_count: i64,
|
||||
pub version: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChapterProps {
|
||||
pub chapter_id: String,
|
||||
pub title: String,
|
||||
pub chapter_order: i64,
|
||||
pub chapter_content: Option<ChapterContent>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CompanionContent {
|
||||
pub version: i64,
|
||||
pub content: String,
|
||||
pub words_count: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedChapter {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
pub contents: Vec<SyncedChapterContent>,
|
||||
pub info: Option<SyncedChapterInfo>,
|
||||
}
|
||||
|
||||
pub struct SyncedChapterContent {
|
||||
pub id: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedChapterInfo {
|
||||
pub id: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CompleteChapterContent {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub content: String,
|
||||
pub order: i64,
|
||||
pub version: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChapterExportInfo {
|
||||
pub chapter_id: String,
|
||||
pub title: String,
|
||||
pub chapter_order: i64,
|
||||
pub available_versions: Vec<i64>,
|
||||
}
|
||||
|
||||
pub use crate::domains::act::service::ActChapter;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IncidentStory {
|
||||
pub incident_title: String,
|
||||
pub incident_summary: String,
|
||||
pub chapter_summary: String,
|
||||
pub chapter_goal: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PlotPointStory {
|
||||
pub plot_title: String,
|
||||
pub plot_summary: String,
|
||||
pub chapter_summary: String,
|
||||
pub chapter_goal: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ActStory {
|
||||
pub act_id: i64,
|
||||
pub summary: String,
|
||||
pub chapter_summary: String,
|
||||
pub chapter_goal: String,
|
||||
pub incidents: Vec<IncidentStory>,
|
||||
pub plot_points: Vec<PlotPointStory>,
|
||||
}
|
||||
|
||||
struct TipTapMark {
|
||||
mark_type: String,
|
||||
attrs: Option<serde_json::Map<String, Value>>,
|
||||
}
|
||||
|
||||
struct TipTapNode {
|
||||
node_type: Option<String>,
|
||||
text: Option<String>,
|
||||
content: Option<Vec<TipTapNode>>,
|
||||
attrs: Option<serde_json::Map<String, Value>>,
|
||||
marks: Option<Vec<TipTapMark>>,
|
||||
}
|
||||
|
||||
/// Retrieves all chapters from a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns an array of ChapterProps containing chapter details.
|
||||
pub fn get_all_chapters_from_a_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<ChapterProps>> {
|
||||
let chapter_query_results: Vec<repo::ChapterQueryResult> = repo::fetch_all_chapter_from_a_book(conn, user_id, book_id, lang)?;
|
||||
let mut decrypted_chapters: Vec<ChapterProps> = Vec::new();
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
for chapter_result in chapter_query_results {
|
||||
let decrypted_title: String = decrypt_data_with_user_key(&chapter_result.title, &user_encryption_key)?;
|
||||
decrypted_chapters.push(ChapterProps {
|
||||
chapter_id: chapter_result.chapter_id,
|
||||
title: decrypted_title,
|
||||
chapter_order: chapter_result.chapter_order,
|
||||
chapter_content: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(decrypted_chapters)
|
||||
}
|
||||
|
||||
/// Retrieves all chapters organized by acts for a specific book.
|
||||
/// Caches decrypted titles to avoid redundant decryption operations.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
|
||||
/// Retrieves a complete chapter with its content for a specific version.
|
||||
/// Optionally updates the last chapter record for the book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `chapter_id` - The unique identifier of the chapter
|
||||
/// * `version` - The version number of the chapter content
|
||||
/// * `book_id` - Optional book identifier to update last chapter record
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns ChapterProps containing chapter details and content.
|
||||
pub fn get_whole_chapter(conn: &Connection, user_id: &str, chapter_id: &str, version: i64, book_id: Option<&str>, lang: Lang) -> AppResult<ChapterProps> {
|
||||
let chapter_content_result: chapter_content_repo::ChapterContentQueryResult = chapter_content_repo::fetch_whole_chapter(conn, user_id, chapter_id, version, lang)?;
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
if let Some(book_id_value) = book_id {
|
||||
repo::update_last_chapter_record(conn, user_id, book_id_value, chapter_id, version, lang)?;
|
||||
}
|
||||
|
||||
Ok(ChapterProps {
|
||||
chapter_id: chapter_content_result.chapter_id,
|
||||
title: decrypt_data_with_user_key(&chapter_content_result.title, &user_encryption_key)?,
|
||||
chapter_order: chapter_content_result.chapter_order,
|
||||
chapter_content: Some(ChapterContent {
|
||||
content: if chapter_content_result.content.is_empty() { String::new() } else { decrypt_data_with_user_key(&chapter_content_result.content, &user_encryption_key)? },
|
||||
version,
|
||||
words_count: chapter_content_result.words_count,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// Saves the content of a chapter for a specific version.
|
||||
/// Encrypts the content before storing it in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `chapter_id` - The unique identifier of the chapter
|
||||
/// * `version` - The version number of the chapter content
|
||||
/// * `content` - The JSON content to save
|
||||
/// * `words_count` - The word count of the content
|
||||
/// * `current_time` - The current timestamp (unused, actual timestamp is generated)
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the content was saved successfully, false otherwise.
|
||||
pub fn save_chapter_content(conn: &Connection, user_id: &str, chapter_id: &str, version: i64, content: &str, words_count: i64, _current_time: i64, lang: Lang) -> AppResult<bool> {
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
let encrypted_content: String = encrypt_data_with_user_key(content, &user_encryption_key)?;
|
||||
chapter_content_repo::update_chapter_content(conn, user_id, chapter_id, version, &encrypted_content, words_count, timestamp_in_seconds(), lang)
|
||||
}
|
||||
|
||||
/// Retrieves the last accessed chapter for a specific book.
|
||||
/// Falls back to the first chapter content if no last chapter record exists.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns ChapterProps containing chapter details and content, or None if no chapters exist.
|
||||
pub fn get_last_chapter(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Option<ChapterProps>> {
|
||||
let last_chapter_record: Option<repo::LastChapterResult> = repo::fetch_last_chapter(conn, user_id, book_id, lang)?;
|
||||
|
||||
if let Some(last_chapter) = last_chapter_record {
|
||||
let chapter_props: ChapterProps = get_whole_chapter(conn, user_id, &last_chapter.chapter_id, last_chapter.version, Some(book_id), lang)?;
|
||||
return Ok(Some(chapter_props));
|
||||
}
|
||||
|
||||
let chapter_content_results: Vec<chapter_content_repo::ChapterContentQueryResult> = chapter_content_repo::fetch_last_chapter_content(conn, user_id, book_id, lang)?;
|
||||
|
||||
if chapter_content_results.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let first_chapter_content: &chapter_content_repo::ChapterContentQueryResult = &chapter_content_results[0];
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
Ok(Some(ChapterProps {
|
||||
chapter_id: first_chapter_content.chapter_id.clone(),
|
||||
title: if first_chapter_content.title.is_empty() { String::new() } else { decrypt_data_with_user_key(&first_chapter_content.title, &user_encryption_key)? },
|
||||
chapter_order: first_chapter_content.chapter_order,
|
||||
chapter_content: Some(ChapterContent {
|
||||
content: if first_chapter_content.content.is_empty() { String::new() } else { decrypt_data_with_user_key(&first_chapter_content.content, &user_encryption_key)? },
|
||||
version: first_chapter_content.version,
|
||||
words_count: first_chapter_content.words_count,
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Adds a new chapter to a book.
|
||||
/// Validates that the chapter name is unique within the book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `title` - The title of the new chapter
|
||||
/// * `words_count` - The initial word count of the chapter
|
||||
/// * `chapter_order` - The order position of the chapter
|
||||
/// * `lang` - The language for error messages
|
||||
/// * `existing_chapter_id` - Optional existing chapter ID for updates
|
||||
/// Returns the unique identifier of the created chapter.
|
||||
/// Errors if a chapter with the same name already exists.
|
||||
pub fn add_chapter(conn: &Connection, user_id: &str, book_id: &str, title: &str, words_count: i64, chapter_order: i64, lang: Lang, existing_chapter_id: Option<&str>) -> AppResult<String> {
|
||||
let hashed_title: String = hash_element(title);
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
let encrypted_title: String = encrypt_data_with_user_key(title, &user_encryption_key)?;
|
||||
|
||||
if existing_chapter_id.is_none() && repo::check_name_duplication(conn, user_id, book_id, &hashed_title, lang)? {
|
||||
return Err(AppError::Internal(if lang == Lang::Fr { "Ce nom de chapitre existe déjà.".to_string() } else { "This chapter name already exists.".to_string() }));
|
||||
}
|
||||
|
||||
let chapter_id: String = create_unique_id(existing_chapter_id);
|
||||
repo::insert_chapter(conn, &chapter_id, user_id, book_id, &encrypted_title, &hashed_title, words_count, chapter_order, lang)
|
||||
}
|
||||
|
||||
/// Removes a chapter from the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `chapter_id` - The unique identifier of the chapter to remove
|
||||
/// * `deleted_at` - The timestamp of deletion
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the chapter was removed successfully, false otherwise.
|
||||
pub fn remove_chapter(conn: &Connection, user_id: &str, book_id: &str, chapter_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
|
||||
let deleted: bool = repo::delete_chapter(conn, user_id, chapter_id, lang)?;
|
||||
if deleted {
|
||||
tombstone_repo::insert(conn, book_id, "book_chapters", chapter_id, Some(book_id), user_id, deleted_at, lang)?;
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
/// Adds chapter information linking a chapter to an act, plot point, and/or incident.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `chapter_id` - The unique identifier of the chapter
|
||||
/// * `act_id` - The act number the chapter belongs to
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `plot_id` - Optional plot point identifier
|
||||
/// * `incident_id` - Optional incident identifier
|
||||
/// * `lang` - The language for error messages
|
||||
/// * `existing_chapter_info_id` - Optional existing chapter info ID for updates
|
||||
/// Returns the unique identifier of the created chapter information.
|
||||
pub fn add_chapter_information(conn: &Connection, user_id: &str, chapter_id: &str, act_id: i64, book_id: &str, plot_id: Option<&str>, incident_id: Option<&str>, lang: Lang, existing_chapter_info_id: Option<&str>) -> AppResult<String> {
|
||||
let chapter_info_id: String = create_unique_id(existing_chapter_info_id);
|
||||
repo::insert_chapter_information(conn, &chapter_info_id, user_id, chapter_id, act_id, book_id, plot_id, incident_id, lang)
|
||||
}
|
||||
|
||||
/// Updates a chapter's title and order position.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `chapter_id` - The unique identifier of the chapter
|
||||
/// * `title` - The new title for the chapter
|
||||
/// * `chapter_order` - The new order position for the chapter
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the chapter was updated successfully, false otherwise.
|
||||
pub fn update_chapter(conn: &Connection, user_id: &str, chapter_id: &str, title: &str, chapter_order: i64, lang: Lang) -> AppResult<bool> {
|
||||
let hashed_title: String = hash_element(title);
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
let encrypted_title: String = encrypt_data_with_user_key(title, &user_encryption_key)?;
|
||||
repo::update_chapter(conn, user_id, chapter_id, &encrypted_title, &hashed_title, chapter_order, timestamp_in_seconds(), lang)
|
||||
}
|
||||
|
||||
/// Updates chapter information for multiple chapters including summary and goal.
|
||||
/// * `conn` - Database connection
|
||||
/// * `chapters` - Array of ActChapter objects containing updated information
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `act_id` - The act number the chapters belong to
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `incident_id` - Optional incident identifier
|
||||
/// * `plot_id` - Optional plot point identifier
|
||||
/// * `lang` - The language for error messages
|
||||
pub fn update_chapter_infos(conn: &Connection, chapters: &[ActChapter], user_id: &str, act_id: i64, book_id: &str, incident_id: Option<&str>, plot_id: Option<&str>, lang: Lang) -> AppResult<()> {
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
for chapter_data in chapters {
|
||||
let encrypted_summary: String = if chapter_data.summary.is_empty() { String::new() } else { encrypt_data_with_user_key(&chapter_data.summary, &user_encryption_key)? };
|
||||
let encrypted_goal: String = if chapter_data.goal.is_empty() { String::new() } else { encrypt_data_with_user_key(&chapter_data.goal, &user_encryption_key)? };
|
||||
let chapter_id: &str = &chapter_data.chapter_id;
|
||||
repo::update_chapter_infos(conn, user_id, chapter_id, act_id, book_id, incident_id, plot_id, &encrypted_summary, Some(&encrypted_goal), timestamp_in_seconds(), lang)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieves the companion content for a chapter (previous version content).
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `chapter_id` - The unique identifier of the chapter
|
||||
/// * `version` - The current version number (companion is version - 1)
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns CompanionContent containing the previous version's content.
|
||||
pub fn get_companion_content(conn: &Connection, user_id: &str, chapter_id: &str, version: i64, lang: Lang) -> AppResult<CompanionContent> {
|
||||
let companion_version: i64 = version - 1;
|
||||
let companion_content_results: Vec<chapter_content_repo::CompanionContentQueryResult> = chapter_content_repo::fetch_companion_content(conn, user_id, chapter_id, companion_version, lang)?;
|
||||
|
||||
if companion_content_results.is_empty() {
|
||||
return Ok(CompanionContent {
|
||||
version,
|
||||
content: String::new(),
|
||||
words_count: 0,
|
||||
});
|
||||
}
|
||||
|
||||
let companion_content_data: &chapter_content_repo::CompanionContentQueryResult = &companion_content_results[0];
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
Ok(CompanionContent {
|
||||
version: companion_content_data.version,
|
||||
content: if companion_content_data.content.is_empty() { String::new() } else { decrypt_data_with_user_key(&companion_content_data.content, &user_encryption_key)? },
|
||||
words_count: companion_content_data.words_count,
|
||||
})
|
||||
}
|
||||
|
||||
/// Retrieves the story context for a chapter including act summaries, incidents, and plot points.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `chapter_id` - The unique identifier of the chapter
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns an array of ActStory containing story context organized by act.
|
||||
pub fn get_chapter_story(conn: &Connection, user_id: &str, chapter_id: &str, lang: Lang) -> AppResult<Vec<ActStory>> {
|
||||
let chapter_story_results: Vec<repo::ChapterStoryQueryResult> = repo::fetch_chapter_story(conn, user_id, chapter_id, lang)?;
|
||||
let mut act_stories_map: HashMap<i64, ActStory> = HashMap::new();
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
for story_result in &chapter_story_results {
|
||||
let act_id: i64 = story_result.act_id;
|
||||
|
||||
if !act_stories_map.contains_key(&act_id) {
|
||||
act_stories_map.insert(act_id, ActStory {
|
||||
act_id,
|
||||
summary: if story_result.summary.is_empty() { String::new() } else { decrypt_data_with_user_key(&story_result.summary, &user_encryption_key)? },
|
||||
chapter_summary: if story_result.chapter_summary.is_empty() { String::new() } else { decrypt_data_with_user_key(&story_result.chapter_summary, &user_encryption_key)? },
|
||||
chapter_goal: if story_result.chapter_goal.is_empty() { String::new() } else { decrypt_data_with_user_key(&story_result.chapter_goal, &user_encryption_key)? },
|
||||
incidents: Vec::new(),
|
||||
plot_points: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(ref _incident_id) = story_result.incident_id {
|
||||
let decrypted_incident_title: String = if let Some(ref incident_title) = story_result.incident_title { decrypt_data_with_user_key(incident_title, &user_encryption_key)? } else { String::new() };
|
||||
let decrypted_incident_summary: String = if let Some(ref incident_summary) = story_result.incident_summary { decrypt_data_with_user_key(incident_summary, &user_encryption_key)? } else { String::new() };
|
||||
|
||||
let act_story = act_stories_map.get(&act_id).unwrap();
|
||||
let incident_already_exists: bool = act_story.incidents.iter().any(
|
||||
|existing_incident| existing_incident.incident_title == decrypted_incident_title && existing_incident.incident_summary == decrypted_incident_summary
|
||||
);
|
||||
|
||||
if !incident_already_exists {
|
||||
let chapter_summary: String = if story_result.chapter_summary.is_empty() { String::new() } else { decrypt_data_with_user_key(&story_result.chapter_summary, &user_encryption_key)? };
|
||||
let chapter_goal: String = if story_result.chapter_goal.is_empty() { String::new() } else { decrypt_data_with_user_key(&story_result.chapter_goal, &user_encryption_key)? };
|
||||
let act_story_mut = act_stories_map.get_mut(&act_id).unwrap();
|
||||
act_story_mut.incidents.push(IncidentStory {
|
||||
incident_title: decrypted_incident_title,
|
||||
incident_summary: decrypted_incident_summary,
|
||||
chapter_summary,
|
||||
chapter_goal,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref _plot_point_id) = story_result.plot_point_id {
|
||||
let decrypted_plot_title: String = if let Some(ref plot_title) = story_result.plot_title { decrypt_data_with_user_key(plot_title, &user_encryption_key)? } else { String::new() };
|
||||
let decrypted_plot_summary: String = if let Some(ref plot_summary) = story_result.plot_summary { decrypt_data_with_user_key(plot_summary, &user_encryption_key)? } else { String::new() };
|
||||
|
||||
let act_story = act_stories_map.get(&act_id).unwrap();
|
||||
let plot_point_already_exists: bool = act_story.plot_points.iter().any(
|
||||
|existing_plot_point| existing_plot_point.plot_title == decrypted_plot_title && existing_plot_point.plot_summary == decrypted_plot_summary
|
||||
);
|
||||
|
||||
if !plot_point_already_exists {
|
||||
let chapter_summary: String = if story_result.chapter_summary.is_empty() { String::new() } else { decrypt_data_with_user_key(&story_result.chapter_summary, &user_encryption_key)? };
|
||||
let chapter_goal: String = if story_result.chapter_goal.is_empty() { String::new() } else { decrypt_data_with_user_key(&story_result.chapter_goal, &user_encryption_key)? };
|
||||
let act_story_mut = act_stories_map.get_mut(&act_id).unwrap();
|
||||
act_story_mut.plot_points.push(PlotPointStory {
|
||||
plot_title: decrypted_plot_title,
|
||||
plot_summary: decrypted_plot_summary,
|
||||
chapter_summary,
|
||||
chapter_goal,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(act_stories_map.into_values().collect())
|
||||
}
|
||||
|
||||
/// Retrieves the content of a specific chapter version.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `chapter_id` - The unique identifier of the chapter
|
||||
/// * `version` - The version number of the content to retrieve
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the decrypted content string, or empty string if not found.
|
||||
pub fn get_chapter_content_by_version(conn: &Connection, user_id: &str, chapter_id: &str, version: i64, lang: Lang) -> AppResult<String> {
|
||||
let content_result: chapter_content_repo::ContentQueryResult = chapter_content_repo::fetch_chapter_content_by_version(conn, user_id, chapter_id, version, lang)?;
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
if content_result.content.is_empty() { Ok(String::new()) } else { Ok(decrypt_data_with_user_key(&content_result.content, &user_encryption_key)?) }
|
||||
}
|
||||
|
||||
/// Removes chapter information by its identifier.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `chapter_info_id` - The unique identifier of the chapter information to remove
|
||||
/// * `deleted_at` - The timestamp of deletion
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the chapter information was removed successfully, false otherwise.
|
||||
pub fn remove_chapter_information(conn: &Connection, user_id: &str, book_id: &str, chapter_info_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
|
||||
let deleted: bool = repo::delete_chapter_information(conn, user_id, chapter_info_id, lang)?;
|
||||
if deleted {
|
||||
tombstone_repo::insert(conn, book_id, "book_chapter_infos", chapter_info_id, Some(book_id), user_id, deleted_at, lang)?;
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
/// Converts TipTap JSON content to HTML string.
|
||||
/// Handles various node types including paragraphs, headings, lists, and text marks.
|
||||
/// * `tip_tap_content` - The TipTap JSON content to convert
|
||||
/// Returns the converted HTML string.
|
||||
pub fn tip_tap_to_html(tip_tap_content: &Value) -> String {
|
||||
fn escape_html_characters(text: &str) -> String {
|
||||
text.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
.replace('\'', "'")
|
||||
}
|
||||
|
||||
fn parse_marks(marks_value: &Value) -> Option<Vec<TipTapMark>> {
|
||||
marks_value.as_array().map(|marks_array| {
|
||||
marks_array.iter().map(|mark_value| {
|
||||
TipTapMark {
|
||||
mark_type: mark_value.get("type").and_then(|t| t.as_str()).unwrap_or("").to_string(),
|
||||
attrs: mark_value.get("attrs").and_then(|a| a.as_object()).cloned(),
|
||||
}
|
||||
}).collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_node(node_value: &Value) -> TipTapNode {
|
||||
let content: Option<Vec<TipTapNode>> = node_value.get("content").and_then(|c| c.as_array()).map(|children| {
|
||||
children.iter().map(|child| parse_node(child)).collect()
|
||||
});
|
||||
TipTapNode {
|
||||
node_type: node_value.get("type").and_then(|t| t.as_str()).map(|s| s.to_string()),
|
||||
text: node_value.get("text").and_then(|t| t.as_str()).map(|s| s.to_string()),
|
||||
content,
|
||||
attrs: node_value.get("attrs").and_then(|a| a.as_object()).cloned(),
|
||||
marks: node_value.get("marks").map(|m| parse_marks(m)).flatten(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_text_with_marks(text: &str, marks: &Option<Vec<TipTapMark>>) -> String {
|
||||
let escaped_text: String = escape_html_characters(text);
|
||||
match marks {
|
||||
None => escaped_text,
|
||||
Some(marks_list) if marks_list.is_empty() => escaped_text,
|
||||
Some(marks_list) => {
|
||||
let mut rendered_text: String = escaped_text;
|
||||
for mark in marks_list {
|
||||
match mark.mark_type.as_str() {
|
||||
"bold" => rendered_text = format!("<strong>{}</strong>", rendered_text),
|
||||
"italic" => rendered_text = format!("<em>{}</em>", rendered_text),
|
||||
"underline" => rendered_text = format!("<u>{}</u>", rendered_text),
|
||||
"strike" => rendered_text = format!("<s>{}</s>", rendered_text),
|
||||
"code" => rendered_text = format!("<code>{}</code>", rendered_text),
|
||||
"link" => {
|
||||
let link_href: String = mark.attrs.as_ref()
|
||||
.and_then(|attrs| attrs.get("href"))
|
||||
.and_then(|href| href.as_str())
|
||||
.map(|href| escape_html_characters(href))
|
||||
.unwrap_or_else(|| "#".to_string());
|
||||
rendered_text = format!("<a href=\"{}\">{}</a>", link_href, rendered_text);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
rendered_text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tip_tap_node(node: &TipTapNode) -> String {
|
||||
if let Some(ref node_type) = node.node_type {
|
||||
if node_type == "text" {
|
||||
let text_content: &str = node.text.as_deref().unwrap_or("\u{00A0}");
|
||||
return render_text_with_marks(text_content, &node.marks);
|
||||
}
|
||||
} else {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
let children_html: String = node.content.as_ref()
|
||||
.map(|children| children.iter().map(|child| render_tip_tap_node(child)).collect::<Vec<String>>().join(""))
|
||||
.unwrap_or_default();
|
||||
|
||||
let text_align_style: String = node.attrs.as_ref()
|
||||
.and_then(|attrs| attrs.get("textAlign"))
|
||||
.and_then(|align| align.as_str())
|
||||
.map(|align| format!(" style=\"text-align: {}\"", align))
|
||||
.unwrap_or_default();
|
||||
|
||||
match node.node_type.as_deref().unwrap_or("") {
|
||||
"doc" => children_html,
|
||||
"paragraph" => {
|
||||
let paragraph_content: &str = if children_html.is_empty() { "\u{00A0}" } else { &children_html };
|
||||
format!("<p{}>{}</p>", text_align_style, paragraph_content)
|
||||
}
|
||||
"heading" => {
|
||||
let heading_level: i64 = node.attrs.as_ref()
|
||||
.and_then(|attrs| attrs.get("level"))
|
||||
.and_then(|level| level.as_i64())
|
||||
.unwrap_or(1);
|
||||
format!("<h{}{}>{}</h{}>", heading_level, text_align_style, children_html, heading_level)
|
||||
}
|
||||
"bulletList" => format!("<ul>{}</ul>", children_html),
|
||||
"orderedList" => format!("<ol>{}</ol>", children_html),
|
||||
"listItem" => format!("<li>{}</li>", children_html),
|
||||
"blockquote" => format!("<blockquote>{}</blockquote>", children_html),
|
||||
"codeBlock" => format!("<pre><code>{}</code></pre>", children_html),
|
||||
"hardBreak" => "<br />".to_string(),
|
||||
"horizontalRule" => "<hr />".to_string(),
|
||||
_ => children_html,
|
||||
}
|
||||
}
|
||||
|
||||
let content_node: TipTapNode = parse_node(tip_tap_content);
|
||||
render_tip_tap_node(&content_node)
|
||||
}
|
||||
|
||||
/// Retrieves all chapters with their content data for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns an array of ChapterContentData containing chapter details with content.
|
||||
|
||||
/// Processes book chapters to return either sheet content or chapter content.
|
||||
/// If only a sheet exists (order -1), returns the sheet. Otherwise, returns all positive-order chapters.
|
||||
/// * `book_chapters` - Array of CompleteChapterContent from the book
|
||||
/// Returns an array of ChapterContentData with processed content.
|
||||
pub fn get_chapters_or_sheet(book_chapters: &[CompleteChapterContent]) -> Vec<ChapterContentData> {
|
||||
let mut processed_chapters: Vec<ChapterContentData> = Vec::new();
|
||||
let sheet_content: Option<&CompleteChapterContent> = book_chapters.iter().find(|chapter| chapter.order == -1);
|
||||
let regular_chapter: Option<&CompleteChapterContent> = book_chapters.iter().find(|chapter| chapter.order > 0);
|
||||
|
||||
if sheet_content.is_some() && regular_chapter.is_none() {
|
||||
let sheet: &CompleteChapterContent = sheet_content.unwrap();
|
||||
let parsed_content: Value = serde_json::from_str(&sheet.content).unwrap_or(Value::Null);
|
||||
processed_chapters.push(ChapterContentData {
|
||||
title: sheet.title.clone(),
|
||||
chapter_order: sheet.order,
|
||||
content: html_to_text(&tip_tap_to_html(&parsed_content)),
|
||||
words_count: 0,
|
||||
version: sheet.version.unwrap_or(0),
|
||||
});
|
||||
} else if regular_chapter.is_some() {
|
||||
for chapter_data in book_chapters {
|
||||
if chapter_data.order < 0 { continue; }
|
||||
let parsed_content: Value = serde_json::from_str(&chapter_data.content).unwrap_or(Value::Null);
|
||||
processed_chapters.push(ChapterContentData {
|
||||
title: chapter_data.title.clone(),
|
||||
chapter_order: chapter_data.order,
|
||||
content: html_to_text(&tip_tap_to_html(&parsed_content)),
|
||||
words_count: 0,
|
||||
version: chapter_data.version.unwrap_or(0),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
processed_chapters
|
||||
}
|
||||
|
||||
/// Retrieves export information for all chapters of a book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns an array of ChapterExportInfo with available versions.
|
||||
pub fn get_chapters_export_info(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<ChapterExportInfo>> {
|
||||
let results: Vec<repo::ChapterExportInfoResult> = repo::fetch_chapters_export_info(conn, user_id, book_id, lang)?;
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
let mut export_infos: Vec<ChapterExportInfo> = Vec::new();
|
||||
|
||||
for result in results {
|
||||
if result.available_versions.is_empty() { continue; }
|
||||
let mut versions: Vec<i64> = result.available_versions
|
||||
.split(',')
|
||||
.filter_map(|version_string| version_string.trim().parse::<i64>().ok())
|
||||
.collect();
|
||||
if versions.is_empty() { continue; }
|
||||
versions.sort();
|
||||
export_infos.push(ChapterExportInfo {
|
||||
chapter_id: result.chapter_id,
|
||||
title: if result.title.is_empty() { String::new() } else { decrypt_data_with_user_key(&result.title, &user_encryption_key)? },
|
||||
chapter_order: result.chapter_order,
|
||||
available_versions: versions,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(export_infos)
|
||||
}
|
||||
|
||||
/// Retrieves complete book data with selected chapter versions.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `selections` - Optional array of chapter selections with specific versions
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns complete book data with the selected chapter contents.
|
||||
pub fn get_complete_book_data_with_selections(conn: &Connection, user_id: &str, book_id: &str, selections: Option<&[repo::ChapterSelectionParam]>, lang: Lang) -> AppResult<book_service::CompleteBookData> {
|
||||
if selections.is_none() || selections.map_or(true, |s| s.is_empty()) {
|
||||
return book_service::complete_book_data(conn, user_id, book_id, lang);
|
||||
}
|
||||
|
||||
let book_data: book_service::CompleteBookData = book_service::complete_book_data(conn, user_id, book_id, lang)?;
|
||||
let selected_results: Vec<repo::SelectedChapterContentResult> = repo::fetch_selected_chapters_content(conn, book_id, selections.unwrap(), lang)?;
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
let mut selected_chapters: Vec<book_service::CompleteChapterContent> = Vec::new();
|
||||
|
||||
for result in selected_results {
|
||||
selected_chapters.push(book_service::CompleteChapterContent {
|
||||
id: result.chapter_id,
|
||||
title: if result.title.is_empty() { String::new() } else { decrypt_data_with_user_key(&result.title, &user_encryption_key)? },
|
||||
content: if result.content.is_empty() { String::new() } else { decrypt_data_with_user_key(&result.content, &user_encryption_key)? },
|
||||
order: result.chapter_order,
|
||||
version: None,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(book_service::CompleteBookData {
|
||||
book_id: book_data.book_id,
|
||||
title: book_data.title,
|
||||
sub_title: book_data.sub_title,
|
||||
summary: book_data.summary,
|
||||
cover_image: book_data.cover_image,
|
||||
user_infos: book_data.user_infos,
|
||||
chapters: selected_chapters,
|
||||
})
|
||||
}
|
||||
2
src-tauri/src/domains/chapter_content/mod.rs
Normal file
2
src-tauri/src/domains/chapter_content/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod repo;
|
||||
pub mod service;
|
||||
302
src-tauri/src/domains/chapter_content/repo.rs
Normal file
302
src-tauri/src/domains/chapter_content/repo.rs
Normal file
@@ -0,0 +1,302 @@
|
||||
use rusqlite::{params, Connection};
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::helpers::create_unique_id;
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
pub struct ChapterContentQueryResult {
|
||||
pub chapter_id: String,
|
||||
pub version: i64,
|
||||
pub content: String,
|
||||
pub words_count: i64,
|
||||
pub title: String,
|
||||
pub chapter_order: i64,
|
||||
}
|
||||
|
||||
pub struct ContentQueryResult {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
pub struct CompanionContentQueryResult {
|
||||
pub version: i64,
|
||||
pub content: String,
|
||||
pub words_count: i64,
|
||||
}
|
||||
|
||||
pub struct BookChapterContentTable {
|
||||
pub content_id: String,
|
||||
pub chapter_id: String,
|
||||
pub author_id: String,
|
||||
pub version: i64,
|
||||
pub content: Option<String>,
|
||||
pub words_count: i64,
|
||||
pub time_on_it: i64,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedChapterContentResult {
|
||||
pub content_id: String,
|
||||
pub chapter_id: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
/// Fetches the last chapter content for a given book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `book_id` - The ID of the book
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns an array of chapter content results ordered by chapter order and version descending.
|
||||
pub fn fetch_last_chapter_content(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<ChapterContentQueryResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT book_chapters.chapter_id as chapter_id, COALESCE(book_chapter_content.version, 2) AS version, COALESCE(book_chapter_content.content, '') AS content, COALESCE(book_chapter_content.words_count, 0) AS words_count, book_chapters.title, book_chapters.chapter_order FROM book_chapters LEFT JOIN book_chapter_content ON book_chapters.chapter_id = book_chapter_content.chapter_id WHERE book_chapters.author_id = ?1 AND book_chapters.book_id = ?2 ORDER BY book_chapters.chapter_order DESC, book_chapter_content.version DESC LIMIT 1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le dernier chapitre.".to_string() } else { "Unable to retrieve last chapter.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![user_id, book_id], |query_row| {
|
||||
Ok(ChapterContentQueryResult {
|
||||
chapter_id: query_row.get(0)?, version: query_row.get(1)?,
|
||||
content: query_row.get(2)?, words_count: query_row.get(3)?,
|
||||
title: query_row.get(4)?, chapter_order: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le dernier chapitre.".to_string() } else { "Unable to retrieve last chapter.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le dernier chapitre.".to_string() } else { "Unable to retrieve last chapter.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Updates the content of a chapter. If no existing content is found, inserts a new record.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `chapter_id` - The ID of the chapter
|
||||
/// * `version` - The version number of the content
|
||||
/// * `encrypt_content` - The encrypted content string
|
||||
/// * `words_count` - The word count of the content
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the update or insert was successful.
|
||||
pub fn update_chapter_content(
|
||||
conn: &Connection, user_id: &str, chapter_id: &str, version: i64,
|
||||
encrypt_content: &str, words_count: i64, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute("UPDATE book_chapter_content SET content=?1, words_count=?2, last_update=?3 WHERE chapter_id=?4 AND author_id=?5 AND version=?6", params![encrypt_content, words_count, last_update, chapter_id, user_id, version])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le contenu du chapitre.".to_string() } else { "Unable to update chapter content.".to_string() }))?;
|
||||
|
||||
if update_result > 0 {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let content_id = create_unique_id(None);
|
||||
let insert_result = conn
|
||||
.execute("INSERT INTO book_chapter_content (content_id, chapter_id, author_id, version, content, words_count, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7)", params![content_id, chapter_id, user_id, version, encrypt_content, words_count, last_update])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le contenu du chapitre.".to_string() } else { "Unable to update chapter content.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Fetches companion content for a specific chapter and version.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `chapter_id` - The ID of the chapter
|
||||
/// * `version` - The version number to fetch
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns an array of companion content results.
|
||||
pub fn fetch_companion_content(conn: &Connection, user_id: &str, chapter_id: &str, version: i64, lang: Lang) -> AppResult<Vec<CompanionContentQueryResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT version, content, words_count FROM book_chapter_content WHERE author_id=?1 AND chapter_id=?2 AND version=?3")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu compagnon.".to_string() } else { "Unable to retrieve companion content.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![user_id, chapter_id, version], |query_row| {
|
||||
Ok(CompanionContentQueryResult {
|
||||
version: query_row.get(0)?, content: query_row.get(1)?,
|
||||
words_count: query_row.get(2)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu compagnon.".to_string() } else { "Unable to retrieve companion content.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu compagnon.".to_string() } else { "Unable to retrieve companion content.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Fetches chapter content by its order position within a book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `chapter_order` - The order position of the chapter
|
||||
/// * `book_id` - The ID of the book
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the content query result for the specified chapter.
|
||||
|
||||
/// Fetches chapter content by chapter ID and version number.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `chapter_id` - The ID of the chapter
|
||||
/// * `version` - The version number to fetch
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the content query result for the specified version.
|
||||
pub fn fetch_chapter_content_by_version(conn: &Connection, user_id: &str, chapter_id: &str, version: i64, lang: Lang) -> AppResult<ContentQueryResult> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT content FROM book_chapter_content WHERE author_id=?1 AND chapter_id=?2 AND version=?3")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu du chapitre.".to_string() } else { "Unable to retrieve chapter content.".to_string() }))?;
|
||||
|
||||
let chapter_content = statement
|
||||
.query_row(params![user_id, chapter_id, version], |query_row| {
|
||||
Ok(ContentQueryResult { content: query_row.get(0)? })
|
||||
})
|
||||
.map_err(|error| match error {
|
||||
rusqlite::Error::QueryReturnedNoRows => AppError::NotFound(if lang == Lang::Fr { "Aucun chapitre trouvé avec cette version.".to_string() } else { "No chapter found with this version.".to_string() }),
|
||||
_ => AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu du chapitre.".to_string() } else { "Unable to retrieve chapter content.".to_string() }),
|
||||
})?;
|
||||
|
||||
Ok(chapter_content)
|
||||
}
|
||||
|
||||
/// Checks whether chapter content exists for a given content ID and user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `content_id` - The ID of the content to check
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the chapter content exists, false otherwise.
|
||||
pub fn is_chapter_content_exist(conn: &Connection, user_id: &str, content_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT 1 FROM book_chapter_content WHERE content_id=?1 AND author_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du contenu du chapitre.".to_string() } else { "Unable to check chapter content existence.".to_string() }))?;
|
||||
|
||||
let exists = statement
|
||||
.exists(params![content_id, user_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du contenu du chapitre.".to_string() } else { "Unable to check chapter content existence.".to_string() }))?;
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Fetches all chapter contents for a specific chapter belonging to a user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `chapter_id` - The ID of the chapter
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns an array of book chapter content records.
|
||||
pub fn fetch_book_chapter_contents(conn: &Connection, user_id: &str, chapter_id: &str, lang: Lang) -> AppResult<Vec<BookChapterContentTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update FROM book_chapter_content WHERE author_id=?1 AND chapter_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres.".to_string() } else { "Unable to retrieve chapter contents.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![user_id, chapter_id], |query_row| {
|
||||
Ok(BookChapterContentTable {
|
||||
content_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
|
||||
author_id: query_row.get(2)?, version: query_row.get(3)?,
|
||||
content: query_row.get(4)?, words_count: query_row.get(5)?,
|
||||
time_on_it: query_row.get(6)?, last_update: query_row.get(7)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres.".to_string() } else { "Unable to retrieve chapter contents.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres.".to_string() } else { "Unable to retrieve chapter contents.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Fetches all synced chapter contents for a user (content ID, chapter ID, and last update timestamp).
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns an array of synced chapter content results.
|
||||
pub fn fetch_synced_chapter_contents(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedChapterContentResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT content_id, chapter_id, last_update FROM book_chapter_content WHERE author_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres synchronisés.".to_string() } else { "Unable to retrieve synced chapter contents.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedChapterContentResult {
|
||||
content_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
|
||||
last_update: query_row.get(2)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres synchronisés.".to_string() } else { "Unable to retrieve synced chapter contents.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu des chapitres synchronisés.".to_string() } else { "Unable to retrieve synced chapter contents.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Inserts a new chapter content record during synchronization.
|
||||
/// * `conn` - Database connection
|
||||
/// * `content_id` - The unique ID for the content
|
||||
/// * `chapter_id` - The ID of the chapter
|
||||
/// * `author_id` - The ID of the author
|
||||
/// * `version` - The version number of the content
|
||||
/// * `content` - The content string (can be null)
|
||||
/// * `words_count` - The word count of the content
|
||||
/// * `time_on_it` - The time spent on this content
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the insert was successful.
|
||||
pub fn insert_sync_chapter_content(
|
||||
conn: &Connection, content_id: &str, chapter_id: &str, author_id: &str, version: i64,
|
||||
content: Option<&str>, words_count: i64, time_on_it: i64, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute("INSERT INTO book_chapter_content (content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", params![content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le contenu du chapitre.".to_string() } else { "Unable to insert chapter content.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Fetches the complete chapter content record by its content ID.
|
||||
/// * `conn` - Database connection
|
||||
/// * `content_id` - The ID of the content to fetch
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns an array of book chapter content records.
|
||||
pub fn fetch_complete_chapter_content_by_id(conn: &Connection, content_id: &str, lang: Lang) -> AppResult<Vec<BookChapterContentTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT content_id, chapter_id, author_id, version, content, words_count, time_on_it, last_update FROM book_chapter_content WHERE content_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu de chapitre complet.".to_string() } else { "Unable to retrieve complete chapter content.".to_string() }))?;
|
||||
|
||||
let rows = statement
|
||||
.query_map(params![content_id], |query_row| {
|
||||
Ok(BookChapterContentTable {
|
||||
content_id: query_row.get(0)?, chapter_id: query_row.get(1)?,
|
||||
author_id: query_row.get(2)?, version: query_row.get(3)?,
|
||||
content: query_row.get(4)?, words_count: query_row.get(5)?,
|
||||
time_on_it: query_row.get(6)?, last_update: query_row.get(7)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu de chapitre complet.".to_string() } else { "Unable to retrieve complete chapter content.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le contenu de chapitre complet.".to_string() } else { "Unable to retrieve complete chapter content.".to_string() }))?;
|
||||
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Fetches a complete chapter with its content by joining chapters and chapter content tables.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `chapter_id` - The ID of the chapter
|
||||
/// * `version` - The version number of the content to fetch
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the chapter content query result with chapter metadata.
|
||||
pub fn fetch_whole_chapter(conn: &Connection, user_id: &str, chapter_id: &str, version: i64, lang: Lang) -> AppResult<ChapterContentQueryResult> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT chapter.chapter_id as chapter_id, chapter.title as title, chapter.chapter_order, chapter.words_count, content.content AS content, content.version as version FROM book_chapters AS chapter LEFT JOIN book_chapter_content AS content ON content.chapter_id = chapter.chapter_id AND content.version = ?1 WHERE chapter.chapter_id = ?2 AND chapter.author_id = ?3")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le chapitre.".to_string() } else { "Unable to retrieve chapter.".to_string() }))?;
|
||||
|
||||
let whole_chapter = statement
|
||||
.query_row(params![version, chapter_id, user_id], |query_row| {
|
||||
Ok(ChapterContentQueryResult {
|
||||
chapter_id: query_row.get(0)?, title: query_row.get(1)?,
|
||||
chapter_order: query_row.get(2)?, words_count: query_row.get(3)?,
|
||||
content: query_row.get::<_, Option<String>>(4)?.unwrap_or_default(),
|
||||
version: query_row.get::<_, Option<i64>>(5)?.unwrap_or(2),
|
||||
})
|
||||
})
|
||||
.map_err(|error| match error {
|
||||
rusqlite::Error::QueryReturnedNoRows => AppError::NotFound(if lang == Lang::Fr { "Aucun chapitre trouvé avec cet ID.".to_string() } else { "No chapter found with this ID.".to_string() }),
|
||||
_ => AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le chapitre.".to_string() } else { "Unable to retrieve chapter.".to_string() }),
|
||||
})?;
|
||||
|
||||
Ok(whole_chapter)
|
||||
}
|
||||
1
src-tauri/src/domains/chapter_content/service.rs
Normal file
1
src-tauri/src/domains/chapter_content/service.rs
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
123
src-tauri/src/domains/character/commands.rs
Normal file
123
src-tauri/src/domains/character/commands.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use serde::Deserialize;
|
||||
use tauri::State;
|
||||
|
||||
use crate::db::connection::DbManager;
|
||||
use crate::domains::character::service;
|
||||
use crate::error::AppError;
|
||||
use crate::shared::session::SessionState;
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
fn get_session(session: &State<SessionState>) -> Result<(String, Lang), AppError> {
|
||||
let session_guard = session.lock().map_err(|e| AppError::Internal(format!("Session lock failed: {}", e)))?;
|
||||
let user_id = session_guard.get_user_id().map_err(|e| AppError::Auth(e))?.to_string();
|
||||
let lang = session_guard.lang;
|
||||
Ok((user_id, lang))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCharacterListData {
|
||||
pub book_id: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_character_list(data: GetCharacterListData, db: State<DbManager>, session: State<SessionState>) -> Result<service::CharacterListResponse, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_character_list(conn, &user_id, &data.book_id, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetCharacterAttributesData {
|
||||
pub character_id: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_character_attributes(data: GetCharacterAttributesData, db: State<DbManager>, session: State<SessionState>) -> Result<Vec<service::CharacterAttribute>, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_attributes(conn, &data.character_id, &user_id, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateCharacterData {
|
||||
pub character: service::CharacterPropsPost,
|
||||
pub book_id: String,
|
||||
pub id: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_character(data: CreateCharacterData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::add_new_character(conn, &user_id, &data.character, &data.book_id, lang, data.id.as_deref())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddCharacterAttributeData {
|
||||
pub character_id: String,
|
||||
pub r#type: String,
|
||||
pub name: String,
|
||||
pub id: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_character_attribute(data: AddCharacterAttributeData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::add_new_attribute(conn, &data.character_id, &user_id, &data.r#type, &data.name, lang, data.id.as_deref())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteCharacterAttributeData {
|
||||
pub attribute_id: String,
|
||||
pub book_id: String,
|
||||
pub deleted_at: i64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_character_attribute(data: DeleteCharacterAttributeData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::delete_attribute(conn, &user_id, &data.book_id, &data.attribute_id, data.deleted_at, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateCharacterData {
|
||||
pub character: service::CharacterPropsPost,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_character(data: UpdateCharacterData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::update_character(conn, &user_id, &data.character, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteCharacterData {
|
||||
pub character_id: String,
|
||||
pub book_id: String,
|
||||
pub deleted_at: i64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_character(data: DeleteCharacterData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::delete_character(conn, &user_id, &data.book_id, &data.character_id, data.deleted_at, lang)
|
||||
}
|
||||
3
src-tauri/src/domains/character/mod.rs
Normal file
3
src-tauri/src/domains/character/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod commands;
|
||||
pub mod repo;
|
||||
pub mod service;
|
||||
663
src-tauri/src/domains/character/repo.rs
Normal file
663
src-tauri/src/domains/character/repo.rs
Normal file
@@ -0,0 +1,663 @@
|
||||
use rusqlite::{params, Connection};
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
pub struct BookCharactersTable {
|
||||
pub character_id: String,
|
||||
pub book_id: String,
|
||||
pub user_id: String,
|
||||
pub first_name: String,
|
||||
pub last_name: Option<String>,
|
||||
pub nickname: Option<String>,
|
||||
pub age: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub species: Option<String>,
|
||||
pub nationality: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub category: String,
|
||||
pub title: Option<String>,
|
||||
pub image: Option<String>,
|
||||
pub role: Option<String>,
|
||||
pub biography: Option<String>,
|
||||
pub history: Option<String>,
|
||||
pub speech_pattern: Option<String>,
|
||||
pub catchphrase: Option<String>,
|
||||
pub residence: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedCharacterResult {
|
||||
pub character_id: String,
|
||||
pub book_id: String,
|
||||
pub first_name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedCharacterAttributeResult {
|
||||
pub attr_id: String,
|
||||
pub character_id: String,
|
||||
pub attribute_name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct BookCharactersAttributesTable {
|
||||
pub attr_id: String,
|
||||
pub character_id: String,
|
||||
pub user_id: String,
|
||||
pub attribute_name: String,
|
||||
pub attribute_value: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct CharacterResult {
|
||||
pub character_id: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub nickname: Option<String>,
|
||||
pub age: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub species: Option<String>,
|
||||
pub nationality: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub title: String,
|
||||
pub category: String,
|
||||
pub image: String,
|
||||
pub role: String,
|
||||
pub biography: String,
|
||||
pub history: String,
|
||||
pub speech_pattern: Option<String>,
|
||||
pub catchphrase: Option<String>,
|
||||
pub residence: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub series_character_id: Option<String>,
|
||||
}
|
||||
|
||||
pub struct AttributeResult {
|
||||
pub attr_id: String,
|
||||
pub attribute_name: String,
|
||||
pub attribute_value: String,
|
||||
}
|
||||
|
||||
pub struct CompleteCharacterResult {
|
||||
pub character_id: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub nickname: Option<String>,
|
||||
pub age: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub species: Option<String>,
|
||||
pub nationality: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub category: String,
|
||||
pub title: String,
|
||||
pub role: String,
|
||||
pub biography: String,
|
||||
pub history: String,
|
||||
pub speech_pattern: Option<String>,
|
||||
pub catchphrase: Option<String>,
|
||||
pub residence: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub attribute_name: String,
|
||||
pub attribute_value: String,
|
||||
}
|
||||
|
||||
pub struct CharacterData {
|
||||
pub first_name: String,
|
||||
pub last_name: Option<String>,
|
||||
pub nickname: Option<String>,
|
||||
pub age: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub species: Option<String>,
|
||||
pub nationality: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub category: Option<String>,
|
||||
pub image: Option<String>,
|
||||
pub role: Option<String>,
|
||||
pub biography: Option<String>,
|
||||
pub history: Option<String>,
|
||||
pub speech_pattern: Option<String>,
|
||||
pub catchphrase: Option<String>,
|
||||
pub residence: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
pub struct SyncCharacterData {
|
||||
pub first_name: String,
|
||||
pub last_name: Option<String>,
|
||||
pub nickname: Option<String>,
|
||||
pub age: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub species: Option<String>,
|
||||
pub nationality: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub category: String,
|
||||
pub title: Option<String>,
|
||||
pub image: Option<String>,
|
||||
pub role: Option<String>,
|
||||
pub biography: Option<String>,
|
||||
pub history: Option<String>,
|
||||
pub speech_pattern: Option<String>,
|
||||
pub catchphrase: Option<String>,
|
||||
pub residence: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
/// Fetches all characters for a specific book and user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of character results.
|
||||
pub fn fetch_characters(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<CharacterResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT character_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, series_character_id FROM book_characters WHERE book_id=?1 AND user_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages.".to_string() } else { "Unable to retrieve characters.".to_string() }))?;
|
||||
|
||||
let characters = statement
|
||||
.query_map(params![book_id, user_id], |query_row| {
|
||||
Ok(CharacterResult {
|
||||
character_id: query_row.get(0)?, first_name: query_row.get(1)?,
|
||||
last_name: query_row.get(2)?, nickname: query_row.get(3)?,
|
||||
age: query_row.get(4)?, gender: query_row.get(5)?,
|
||||
species: query_row.get(6)?, nationality: query_row.get(7)?,
|
||||
status: query_row.get(8)?, title: query_row.get(9)?,
|
||||
category: query_row.get(10)?, image: query_row.get(11)?,
|
||||
role: query_row.get(12)?, biography: query_row.get(13)?,
|
||||
history: query_row.get(14)?, speech_pattern: query_row.get(15)?,
|
||||
catchphrase: query_row.get(16)?, residence: query_row.get(17)?,
|
||||
notes: query_row.get(18)?, color: query_row.get(19)?,
|
||||
series_character_id: query_row.get(20)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages.".to_string() } else { "Unable to retrieve characters.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages.".to_string() } else { "Unable to retrieve characters.".to_string() }))?;
|
||||
|
||||
Ok(characters)
|
||||
}
|
||||
|
||||
/// Adds a new character to the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_id` - The unique identifier for the new character
|
||||
/// * `character_data` - Object containing all encrypted character fields
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// * `series_character_id` - Optional series character identifier
|
||||
/// Returns the character ID if successful.
|
||||
pub fn add_new_character(
|
||||
conn: &Connection, user_id: &str, character_id: &str, character_data: &CharacterData,
|
||||
book_id: &str, lang: Lang, series_character_id: Option<&str>, last_update: i64,
|
||||
) -> AppResult<String> {
|
||||
let insert_result = if let Some(series_id) = series_character_id {
|
||||
conn.execute(
|
||||
"INSERT INTO book_characters (character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, series_character_id, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21,?22,?23,?24)",
|
||||
params![character_id, book_id, user_id, character_data.first_name, character_data.last_name, character_data.nickname, character_data.age, character_data.gender, character_data.species, character_data.nationality, character_data.status, character_data.category, character_data.title, character_data.image, character_data.role, character_data.biography, character_data.history, character_data.speech_pattern, character_data.catchphrase, character_data.residence, character_data.notes, character_data.color, series_id, last_update],
|
||||
)
|
||||
} else {
|
||||
conn.execute(
|
||||
"INSERT INTO book_characters (character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21,?22,?23)",
|
||||
params![character_id, book_id, user_id, character_data.first_name, character_data.last_name, character_data.nickname, character_data.age, character_data.gender, character_data.species, character_data.nationality, character_data.status, character_data.category, character_data.title, character_data.image, character_data.role, character_data.biography, character_data.history, character_data.speech_pattern, character_data.catchphrase, character_data.residence, character_data.notes, character_data.color, last_update],
|
||||
)
|
||||
};
|
||||
|
||||
let insert_result = insert_result
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le personnage.".to_string() } else { "Unable to add character.".to_string() }))?;
|
||||
|
||||
if insert_result > 0 {
|
||||
Ok(character_id.to_string())
|
||||
} else {
|
||||
Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de l'ajout du personnage.".to_string() } else { "Error adding character.".to_string() }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a new attribute for a character.
|
||||
/// * `conn` - Database connection
|
||||
/// * `attribute_id` - The unique identifier for the new attribute
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `attribute_type` - The attribute name/type
|
||||
/// * `name` - The attribute value
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the attribute ID if successful.
|
||||
pub fn insert_attribute(
|
||||
conn: &Connection, attribute_id: &str, character_id: &str, user_id: &str,
|
||||
attribute_type: &str, name: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<String> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO book_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?1,?2,?3,?4,?5,?6)",
|
||||
params![attribute_id, character_id, user_id, attribute_type, name, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter l'attribut.".to_string() } else { "Unable to add attribute.".to_string() }))?;
|
||||
|
||||
if insert_result > 0 {
|
||||
Ok(attribute_id.to_string())
|
||||
} else {
|
||||
Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de l'ajout de l'attribut.".to_string() } else { "Error adding attribute.".to_string() }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates an existing character's information.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `id` - The unique identifier of the character to update
|
||||
/// * `character_data` - Object containing all encrypted character fields
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// * `series_character_id` - Optional series character identifier
|
||||
/// Returns true if the update was successful, false otherwise.
|
||||
pub fn update_character(
|
||||
conn: &Connection, user_id: &str, id: &str, character_data: &CharacterData,
|
||||
last_update: i64, lang: Lang, series_character_id: Option<&str>,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = if let Some(series_id) = series_character_id {
|
||||
conn.execute(
|
||||
"UPDATE book_characters SET first_name=?1, last_name=?2, nickname=?3, age=?4, gender=?5, species=?6, nationality=?7, status=?8, title=?9, category=?10, image=?11, role=?12, biography=?13, history=?14, speech_pattern=?15, catchphrase=?16, residence=?17, notes=?18, color=?19, series_character_id=?20, last_update=?21 WHERE character_id=?22 AND user_id=?23",
|
||||
params![character_data.first_name, character_data.last_name, character_data.nickname, character_data.age, character_data.gender, character_data.species, character_data.nationality, character_data.status, character_data.title, character_data.category, character_data.image, character_data.role, character_data.biography, character_data.history, character_data.speech_pattern, character_data.catchphrase, character_data.residence, character_data.notes, character_data.color, series_id, last_update, id, user_id],
|
||||
)
|
||||
} else {
|
||||
conn.execute(
|
||||
"UPDATE book_characters SET first_name=?1, last_name=?2, nickname=?3, age=?4, gender=?5, species=?6, nationality=?7, status=?8, title=?9, category=?10, image=?11, role=?12, biography=?13, history=?14, speech_pattern=?15, catchphrase=?16, residence=?17, notes=?18, color=?19, last_update=?20 WHERE character_id=?21 AND user_id=?22",
|
||||
params![character_data.first_name, character_data.last_name, character_data.nickname, character_data.age, character_data.gender, character_data.species, character_data.nationality, character_data.status, character_data.title, character_data.category, character_data.image, character_data.role, character_data.biography, character_data.history, character_data.speech_pattern, character_data.catchphrase, character_data.residence, character_data.notes, character_data.color, last_update, id, user_id],
|
||||
)
|
||||
};
|
||||
|
||||
let update_result = update_result
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le personnage.".to_string() } else { "Unable to update character.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Deletes a character and all its related data (attributes) from the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_id` - The unique identifier of the character to delete
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the deletion was successful, false otherwise.
|
||||
pub fn delete_character(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
conn.execute(
|
||||
"DELETE FROM book_characters_attributes WHERE character_id=?1 AND user_id=?2",
|
||||
params![character_id, user_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le personnage.".to_string() } else { "Unable to delete character.".to_string() }))?;
|
||||
|
||||
let delete_result = conn
|
||||
.execute(
|
||||
"DELETE FROM book_characters WHERE character_id=?1 AND user_id=?2",
|
||||
params![character_id, user_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le personnage.".to_string() } else { "Unable to delete character.".to_string() }))?;
|
||||
|
||||
Ok(delete_result > 0)
|
||||
}
|
||||
|
||||
/// Deletes a character attribute from the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `attribute_id` - The unique identifier of the attribute to delete
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the deletion was successful, false otherwise.
|
||||
pub fn delete_attribute(conn: &Connection, user_id: &str, attribute_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let delete_result = conn
|
||||
.execute(
|
||||
"DELETE FROM book_characters_attributes WHERE attr_id=?1 AND user_id=?2",
|
||||
params![attribute_id, user_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer l'attribut.".to_string() } else { "Unable to delete attribute.".to_string() }))?;
|
||||
|
||||
Ok(delete_result > 0)
|
||||
}
|
||||
|
||||
/// Fetches all attributes for a specific character.
|
||||
/// * `conn` - Database connection
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of attribute results.
|
||||
pub fn fetch_attributes(conn: &Connection, character_id: &str, user_id: &str, lang: Lang) -> AppResult<Vec<AttributeResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT attr_id, attribute_name, attribute_value FROM book_characters_attributes WHERE character_id=?1 AND user_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs.".to_string() } else { "Unable to retrieve attributes.".to_string() }))?;
|
||||
|
||||
let attributes = statement
|
||||
.query_map(params![character_id, user_id], |query_row| {
|
||||
Ok(AttributeResult { attr_id: query_row.get(0)?, attribute_name: query_row.get(1)?, attribute_value: query_row.get(2)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs.".to_string() } else { "Unable to retrieve attributes.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs.".to_string() } else { "Unable to retrieve attributes.".to_string() }))?;
|
||||
|
||||
Ok(attributes)
|
||||
}
|
||||
|
||||
/// Fetches complete character information including attributes, optionally filtered by character IDs.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `tags` - An optional array of character IDs to filter by
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of complete character results with attributes.
|
||||
pub fn fetch_complete_characters(conn: &Connection, user_id: &str, book_id: &str, tags: &[String], lang: Lang) -> AppResult<Vec<CompleteCharacterResult>> {
|
||||
let mut query = "SELECT charac.character_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, role, biography, history, speech_pattern, catchphrase, residence, notes, color, attribute_name, attribute_value FROM book_characters AS charac LEFT JOIN book_characters_attributes AS attr ON charac.character_id=attr.character_id WHERE charac.user_id=?1 AND charac.book_id=?2".to_string();
|
||||
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
|
||||
param_values.push(Box::new(user_id.to_string()));
|
||||
param_values.push(Box::new(book_id.to_string()));
|
||||
|
||||
if !tags.is_empty() {
|
||||
let placeholders: String = tags.iter().enumerate().map(|(index, _)| format!("?{}", index + 3)).collect::<Vec<_>>().join(",");
|
||||
query += &format!(" AND charac.character_id IN ({})", placeholders);
|
||||
for tag in tags {
|
||||
param_values.push(Box::new(tag.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
let param_refs: Vec<&dyn rusqlite::types::ToSql> = param_values.iter().map(|param| param.as_ref()).collect();
|
||||
|
||||
let mut statement = conn
|
||||
.prepare(&query)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages complets.".to_string() } else { "Unable to retrieve complete characters.".to_string() }))?;
|
||||
|
||||
let characters = statement
|
||||
.query_map(param_refs.as_slice(), |query_row| {
|
||||
Ok(CompleteCharacterResult {
|
||||
character_id: query_row.get(0)?, first_name: query_row.get(1)?,
|
||||
last_name: query_row.get(2)?, nickname: query_row.get(3)?,
|
||||
age: query_row.get(4)?, gender: query_row.get(5)?,
|
||||
species: query_row.get(6)?, nationality: query_row.get(7)?,
|
||||
status: query_row.get(8)?, category: query_row.get(9)?,
|
||||
title: query_row.get(10)?, role: query_row.get(11)?,
|
||||
biography: query_row.get(12)?, history: query_row.get(13)?,
|
||||
speech_pattern: query_row.get(14)?, catchphrase: query_row.get(15)?,
|
||||
residence: query_row.get(16)?, notes: query_row.get(17)?,
|
||||
color: query_row.get(18)?, attribute_name: query_row.get(19)?,
|
||||
attribute_value: query_row.get(20)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages complets.".to_string() } else { "Unable to retrieve complete characters.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages complets.".to_string() } else { "Unable to retrieve complete characters.".to_string() }))?;
|
||||
|
||||
if characters.is_empty() {
|
||||
return Err(AppError::NotFound(if lang == Lang::Fr { "Aucun personnage complet trouvé.".to_string() } else { "No complete characters found.".to_string() }));
|
||||
}
|
||||
|
||||
Ok(characters)
|
||||
}
|
||||
|
||||
/// Updates an existing character attribute.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_attribute_id` - The unique identifier of the attribute to update
|
||||
/// * `attribute_name` - The new attribute name
|
||||
/// * `attribute_value` - The new attribute value
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the update was successful, false otherwise.
|
||||
pub fn update_character_attribute(
|
||||
conn: &Connection, user_id: &str, character_attribute_id: &str,
|
||||
attribute_name: &str, attribute_value: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute(
|
||||
"UPDATE book_characters_attributes SET attribute_name=?1, attribute_value=?2, last_update=?3 WHERE attr_id=?4 AND user_id=?5",
|
||||
params![attribute_name, attribute_value, last_update, character_attribute_id, user_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour l'attribut du personnage.".to_string() } else { "Unable to update character attribute.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Checks if a character exists in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_id` - The unique identifier of the character to check
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the character exists, false otherwise.
|
||||
pub fn is_character_exist(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT 1 FROM book_characters WHERE character_id=?1 AND user_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du personnage.".to_string() } else { "Unable to check character existence.".to_string() }))?;
|
||||
|
||||
let exists = statement
|
||||
.query_row(params![character_id, user_id], |_query_row| Ok(true))
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Checks if a character attribute exists in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_attribute_id` - The unique identifier of the attribute to check
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the attribute exists, false otherwise.
|
||||
pub fn is_character_attribute_exist(conn: &Connection, user_id: &str, character_attribute_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT 1 FROM book_characters_attributes WHERE attr_id=?1 AND user_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'attribut du personnage.".to_string() } else { "Unable to check character attribute existence.".to_string() }))?;
|
||||
|
||||
let exists = statement
|
||||
.query_row(params![character_attribute_id, user_id], |_query_row| Ok(true))
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Fetches all characters for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of book characters.
|
||||
pub fn fetch_book_characters(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookCharactersTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM book_characters WHERE user_id=?1 AND book_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages.".to_string() } else { "Unable to retrieve characters.".to_string() }))?;
|
||||
|
||||
let characters = statement
|
||||
.query_map(params![user_id, book_id], |query_row| {
|
||||
Ok(BookCharactersTable {
|
||||
character_id: query_row.get(0)?, book_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, first_name: query_row.get(3)?,
|
||||
last_name: query_row.get(4)?, nickname: query_row.get(5)?,
|
||||
age: query_row.get(6)?, gender: query_row.get(7)?,
|
||||
species: query_row.get(8)?, nationality: query_row.get(9)?,
|
||||
status: query_row.get(10)?, category: query_row.get(11)?,
|
||||
title: query_row.get(12)?, image: query_row.get(13)?,
|
||||
role: query_row.get(14)?, biography: query_row.get(15)?,
|
||||
history: query_row.get(16)?, speech_pattern: query_row.get(17)?,
|
||||
catchphrase: query_row.get(18)?, residence: query_row.get(19)?,
|
||||
notes: query_row.get(20)?, color: query_row.get(21)?,
|
||||
last_update: query_row.get(22)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages.".to_string() } else { "Unable to retrieve characters.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages.".to_string() } else { "Unable to retrieve characters.".to_string() }))?;
|
||||
|
||||
Ok(characters)
|
||||
}
|
||||
|
||||
/// Fetches all attributes for a specific character.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of character attributes.
|
||||
pub fn fetch_book_characters_attributes(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult<Vec<BookCharactersAttributesTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM book_characters_attributes WHERE user_id=?1 AND character_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?;
|
||||
|
||||
let attributes = statement
|
||||
.query_map(params![user_id, character_id], |query_row| {
|
||||
Ok(BookCharactersAttributesTable {
|
||||
attr_id: query_row.get(0)?, character_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, attribute_name: query_row.get(3)?,
|
||||
attribute_value: query_row.get(4)?, last_update: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages.".to_string() } else { "Unable to retrieve character attributes.".to_string() }))?;
|
||||
|
||||
Ok(attributes)
|
||||
}
|
||||
|
||||
/// Fetches all synced characters for a user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of synced character results.
|
||||
pub fn fetch_synced_characters(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedCharacterResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT character_id, book_id, first_name, last_update FROM book_characters WHERE user_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages synchronisés.".to_string() } else { "Unable to retrieve synced characters.".to_string() }))?;
|
||||
|
||||
let synced_characters = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedCharacterResult { character_id: query_row.get(0)?, book_id: query_row.get(1)?, first_name: query_row.get(2)?, last_update: query_row.get(3)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages synchronisés.".to_string() } else { "Unable to retrieve synced characters.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les personnages synchronisés.".to_string() } else { "Unable to retrieve synced characters.".to_string() }))?;
|
||||
|
||||
Ok(synced_characters)
|
||||
}
|
||||
|
||||
/// Fetches all synced character attributes for a user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of synced character attribute results.
|
||||
pub fn fetch_synced_character_attributes(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedCharacterAttributeResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT attr_id, character_id, attribute_name, last_update FROM book_characters_attributes WHERE user_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages synchronisés.".to_string() } else { "Unable to retrieve synced character attributes.".to_string() }))?;
|
||||
|
||||
let synced_attributes = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedCharacterAttributeResult { attr_id: query_row.get(0)?, character_id: query_row.get(1)?, attribute_name: query_row.get(2)?, last_update: query_row.get(3)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages synchronisés.".to_string() } else { "Unable to retrieve synced character attributes.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les attributs des personnages synchronisés.".to_string() } else { "Unable to retrieve synced character attributes.".to_string() }))?;
|
||||
|
||||
Ok(synced_attributes)
|
||||
}
|
||||
|
||||
/// Inserts a synced character into the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_data` - Object containing all character fields
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the insertion was successful, false otherwise.
|
||||
pub fn insert_sync_character(
|
||||
conn: &Connection, character_id: &str, book_id: &str, user_id: &str,
|
||||
character_data: &SyncCharacterData, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO book_characters (character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18,?19,?20,?21,?22,?23)",
|
||||
params![character_id, book_id, user_id, character_data.first_name, character_data.last_name, character_data.nickname, character_data.age, character_data.gender, character_data.species, character_data.nationality, character_data.status, character_data.category, character_data.title, character_data.image, character_data.role, character_data.biography, character_data.history, character_data.speech_pattern, character_data.catchphrase, character_data.residence, character_data.notes, character_data.color, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le personnage.".to_string() } else { "Unable to insert character.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Inserts a synced character attribute into the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `attr_id` - The unique identifier of the attribute
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `attribute_name` - The name of the attribute
|
||||
/// * `attribute_value` - The value of the attribute
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the insertion was successful, false otherwise.
|
||||
pub fn insert_sync_character_attribute(
|
||||
conn: &Connection, attr_id: &str, character_id: &str, user_id: &str,
|
||||
attribute_name: &str, attribute_value: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO book_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?1,?2,?3,?4,?5,?6)",
|
||||
params![attr_id, character_id, user_id, attribute_name, attribute_value, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer l'attribut du personnage.".to_string() } else { "Unable to insert character attribute.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Fetches a complete character by its ID.
|
||||
/// * `conn` - Database connection
|
||||
/// * `id` - The unique identifier of the character
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of book characters (typically one).
|
||||
pub fn fetch_complete_character_by_id(conn: &Connection, id: &str, lang: Lang) -> AppResult<Vec<BookCharactersTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT character_id, book_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM book_characters WHERE character_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le personnage complet.".to_string() } else { "Unable to retrieve complete character.".to_string() }))?;
|
||||
|
||||
let character = statement
|
||||
.query_map(params![id], |query_row| {
|
||||
Ok(BookCharactersTable {
|
||||
character_id: query_row.get(0)?, book_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, first_name: query_row.get(3)?,
|
||||
last_name: query_row.get(4)?, nickname: query_row.get(5)?,
|
||||
age: query_row.get(6)?, gender: query_row.get(7)?,
|
||||
species: query_row.get(8)?, nationality: query_row.get(9)?,
|
||||
status: query_row.get(10)?, category: query_row.get(11)?,
|
||||
title: query_row.get(12)?, image: query_row.get(13)?,
|
||||
role: query_row.get(14)?, biography: query_row.get(15)?,
|
||||
history: query_row.get(16)?, speech_pattern: query_row.get(17)?,
|
||||
catchphrase: query_row.get(18)?, residence: query_row.get(19)?,
|
||||
notes: query_row.get(20)?, color: query_row.get(21)?,
|
||||
last_update: query_row.get(22)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le personnage complet.".to_string() } else { "Unable to retrieve complete character.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le personnage complet.".to_string() } else { "Unable to retrieve complete character.".to_string() }))?;
|
||||
|
||||
Ok(character)
|
||||
}
|
||||
|
||||
/// Fetches a complete character attribute by its ID.
|
||||
/// * `conn` - Database connection
|
||||
/// * `id` - The unique identifier of the attribute
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of character attributes (typically one).
|
||||
pub fn fetch_complete_character_attribute_by_id(conn: &Connection, id: &str, lang: Lang) -> AppResult<Vec<BookCharactersAttributesTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM book_characters_attributes WHERE attr_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'attribut de personnage complet.".to_string() } else { "Unable to retrieve complete character attribute.".to_string() }))?;
|
||||
|
||||
let attribute = statement
|
||||
.query_map(params![id], |query_row| {
|
||||
Ok(BookCharactersAttributesTable {
|
||||
attr_id: query_row.get(0)?, character_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, attribute_name: query_row.get(3)?,
|
||||
attribute_value: query_row.get(4)?, last_update: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'attribut de personnage complet.".to_string() } else { "Unable to retrieve complete character attribute.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'attribut de personnage complet.".to_string() } else { "Unable to retrieve complete character attribute.".to_string() }))?;
|
||||
|
||||
Ok(attribute)
|
||||
}
|
||||
656
src-tauri/src/domains/character/service.rs
Normal file
656
src-tauri/src/domains/character/service.rs
Normal file
@@ -0,0 +1,656 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rusqlite::Connection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::crypto::encryption::{encrypt_data_with_user_key, decrypt_data_with_user_key};
|
||||
use crate::crypto::key_manager::get_user_encryption_key;
|
||||
use crate::domains::book::repo as book_repo;
|
||||
use crate::domains::character::repo;
|
||||
use crate::domains::tombstone::repo as tombstone_repo;
|
||||
use crate::error::AppResult;
|
||||
use crate::helpers::{create_unique_id, timestamp_in_seconds};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterPropsPost {
|
||||
pub id: Option<String>,
|
||||
pub name: String,
|
||||
pub last_name: String,
|
||||
pub nickname: String,
|
||||
pub age: Option<i64>,
|
||||
pub gender: String,
|
||||
pub species: String,
|
||||
pub nationality: String,
|
||||
pub status: String,
|
||||
pub category: String,
|
||||
pub title: String,
|
||||
pub image: String,
|
||||
pub physical: Vec<AttributeName>,
|
||||
pub psychological: Vec<AttributeName>,
|
||||
pub relations: Vec<AttributeName>,
|
||||
pub skills: Vec<AttributeName>,
|
||||
pub weaknesses: Vec<AttributeName>,
|
||||
pub strengths: Vec<AttributeName>,
|
||||
pub goals: Vec<AttributeName>,
|
||||
pub motivations: Vec<AttributeName>,
|
||||
pub arc: Vec<AttributeName>,
|
||||
pub secrets: Vec<AttributeName>,
|
||||
pub fears: Vec<AttributeName>,
|
||||
pub flaws: Vec<AttributeName>,
|
||||
pub beliefs: Vec<AttributeName>,
|
||||
pub conflicts: Vec<AttributeName>,
|
||||
pub quotes: Vec<AttributeName>,
|
||||
pub distinguishing_marks: Vec<AttributeName>,
|
||||
pub items: Vec<AttributeName>,
|
||||
pub affiliations: Vec<AttributeName>,
|
||||
pub role: String,
|
||||
pub biography: Option<String>,
|
||||
pub history: Option<String>,
|
||||
pub speech_pattern: Option<String>,
|
||||
pub catchphrase: Option<String>,
|
||||
pub residence: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub series_character_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AttributeName {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterProps {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_name: String,
|
||||
pub nickname: String,
|
||||
pub age: Option<i64>,
|
||||
pub gender: String,
|
||||
pub species: String,
|
||||
pub nationality: String,
|
||||
pub status: String,
|
||||
pub title: String,
|
||||
pub category: String,
|
||||
pub image: String,
|
||||
pub role: String,
|
||||
pub biography: String,
|
||||
pub history: String,
|
||||
pub speech_pattern: String,
|
||||
pub catchphrase: String,
|
||||
pub residence: String,
|
||||
pub notes: String,
|
||||
pub color: String,
|
||||
pub series_character_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CharacterListResponse {
|
||||
pub characters: Vec<CharacterProps>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
pub struct CompleteCharacterProps {
|
||||
pub id: Option<String>,
|
||||
pub name: String,
|
||||
pub last_name: String,
|
||||
pub nickname: String,
|
||||
pub age: Option<i64>,
|
||||
pub gender: String,
|
||||
pub species: String,
|
||||
pub nationality: String,
|
||||
pub status: String,
|
||||
pub title: String,
|
||||
pub category: String,
|
||||
pub image: Option<String>,
|
||||
pub role: String,
|
||||
pub biography: String,
|
||||
pub history: String,
|
||||
pub speech_pattern: String,
|
||||
pub catchphrase: String,
|
||||
pub residence: String,
|
||||
pub notes: String,
|
||||
pub color: String,
|
||||
pub physical: Vec<Attribute>,
|
||||
pub psychological: Vec<Attribute>,
|
||||
pub relations: Vec<Attribute>,
|
||||
pub skills: Vec<Attribute>,
|
||||
pub weaknesses: Vec<Attribute>,
|
||||
pub strengths: Vec<Attribute>,
|
||||
pub goals: Vec<Attribute>,
|
||||
pub motivations: Vec<Attribute>,
|
||||
pub arc: Vec<Attribute>,
|
||||
pub secrets: Vec<Attribute>,
|
||||
pub fears: Vec<Attribute>,
|
||||
pub flaws: Vec<Attribute>,
|
||||
pub beliefs: Vec<Attribute>,
|
||||
pub conflicts: Vec<Attribute>,
|
||||
pub quotes: Vec<Attribute>,
|
||||
pub distinguishing_marks: Vec<Attribute>,
|
||||
pub items: Vec<Attribute>,
|
||||
pub affiliations: Vec<Attribute>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Attribute {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CharacterAttribute {
|
||||
pub r#type: String,
|
||||
pub values: Vec<Attribute>,
|
||||
}
|
||||
|
||||
pub struct SyncedCharacter {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
pub attributes: Vec<SyncedCharacterAttribute>,
|
||||
}
|
||||
|
||||
pub struct SyncedCharacterAttribute {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
/// Retrieves a list of all characters for a specific book.
|
||||
/// Decrypts character data using the user's encryption key.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language code for localization
|
||||
/// Returns a CharacterListResponse containing decrypted characters and enabled status.
|
||||
pub fn get_character_list(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<CharacterListResponse> {
|
||||
let book_tools: Option<book_repo::BookToolsTable> = book_repo::fetch_book_tools(conn, user_id, book_id, lang)?;
|
||||
let enabled: bool = book_tools.map_or(false, |book_tools_row| book_tools_row.characters_enabled == 1);
|
||||
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let encrypted_characters: Vec<repo::CharacterResult> = repo::fetch_characters(conn, user_id, book_id, lang)?;
|
||||
|
||||
if encrypted_characters.is_empty() {
|
||||
return Ok(CharacterListResponse { characters: vec![], enabled });
|
||||
}
|
||||
|
||||
let mut decrypted_character_list: Vec<CharacterProps> = Vec::with_capacity(encrypted_characters.len());
|
||||
for encrypted_character in encrypted_characters {
|
||||
decrypted_character_list.push(CharacterProps {
|
||||
id: encrypted_character.character_id,
|
||||
name: if encrypted_character.first_name.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.first_name, &user_key)? },
|
||||
last_name: if encrypted_character.last_name.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.last_name, &user_key)? },
|
||||
nickname: if let Some(ref value) = encrypted_character.nickname { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
age: if let Some(ref value) = encrypted_character.age { Some(decrypt_data_with_user_key(value, &user_key)?.parse::<i64>().unwrap_or(0)) } else { None },
|
||||
gender: if let Some(ref value) = encrypted_character.gender { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
species: if let Some(ref value) = encrypted_character.species { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
nationality: if let Some(ref value) = encrypted_character.nationality { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
status: if let Some(ref value) = encrypted_character.status { decrypt_data_with_user_key(value, &user_key)? } else { "alive".to_string() },
|
||||
title: if encrypted_character.title.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.title, &user_key)? },
|
||||
category: if encrypted_character.category.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.category, &user_key)? },
|
||||
image: if encrypted_character.image.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.image, &user_key)? },
|
||||
role: if encrypted_character.role.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.role, &user_key)? },
|
||||
biography: if encrypted_character.biography.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.biography, &user_key)? },
|
||||
history: if encrypted_character.history.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.history, &user_key)? },
|
||||
speech_pattern: if let Some(ref value) = encrypted_character.speech_pattern { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
catchphrase: if let Some(ref value) = encrypted_character.catchphrase { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
residence: if let Some(ref value) = encrypted_character.residence { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
notes: if let Some(ref value) = encrypted_character.notes { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
color: if let Some(ref value) = encrypted_character.color { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
series_character_id: encrypted_character.series_character_id,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(CharacterListResponse { characters: decrypted_character_list, enabled })
|
||||
}
|
||||
|
||||
/// Creates a new character with all its attributes for a specific book.
|
||||
/// Encrypts all character data before storing in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character` - The character data to be created
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language code for localization
|
||||
/// * `existing_character_id` - Optional existing character ID for updates or imports
|
||||
/// Returns the unique identifier of the newly created character.
|
||||
pub fn add_new_character(conn: &Connection, user_id: &str, character: &CharacterPropsPost, book_id: &str, lang: Lang, existing_character_id: Option<&str>) -> AppResult<String> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let character_id: String = create_unique_id(existing_character_id);
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
|
||||
let character_data = repo::CharacterData {
|
||||
first_name: encrypt_data_with_user_key(&character.name, &user_key)?,
|
||||
last_name: Some(encrypt_data_with_user_key(&character.last_name, &user_key)?),
|
||||
nickname: Some(encrypt_data_with_user_key(if character.nickname.is_empty() { "" } else { &character.nickname }, &user_key)?),
|
||||
age: if let Some(age_value) = character.age { Some(encrypt_data_with_user_key(&age_value.to_string(), &user_key)?) } else { None },
|
||||
gender: Some(encrypt_data_with_user_key(if character.gender.is_empty() { "" } else { &character.gender }, &user_key)?),
|
||||
species: Some(encrypt_data_with_user_key(if character.species.is_empty() { "" } else { &character.species }, &user_key)?),
|
||||
nationality: Some(encrypt_data_with_user_key(if character.nationality.is_empty() { "" } else { &character.nationality }, &user_key)?),
|
||||
status: Some(encrypt_data_with_user_key(if character.status.is_empty() { "alive" } else { &character.status }, &user_key)?),
|
||||
title: Some(encrypt_data_with_user_key(&character.title, &user_key)?),
|
||||
category: Some(encrypt_data_with_user_key(&character.category, &user_key)?),
|
||||
image: Some(encrypt_data_with_user_key(&character.image, &user_key)?),
|
||||
role: Some(encrypt_data_with_user_key(&character.role, &user_key)?),
|
||||
biography: Some(encrypt_data_with_user_key(character.biography.as_deref().unwrap_or(""), &user_key)?),
|
||||
history: Some(encrypt_data_with_user_key(character.history.as_deref().unwrap_or(""), &user_key)?),
|
||||
speech_pattern: Some(encrypt_data_with_user_key(character.speech_pattern.as_deref().unwrap_or(""), &user_key)?),
|
||||
catchphrase: Some(encrypt_data_with_user_key(character.catchphrase.as_deref().unwrap_or(""), &user_key)?),
|
||||
residence: Some(encrypt_data_with_user_key(character.residence.as_deref().unwrap_or(""), &user_key)?),
|
||||
notes: Some(encrypt_data_with_user_key(character.notes.as_deref().unwrap_or(""), &user_key)?),
|
||||
color: Some(encrypt_data_with_user_key(character.color.as_deref().unwrap_or(""), &user_key)?),
|
||||
};
|
||||
|
||||
let series_character_id: Option<&str> = character.series_character_id.as_deref();
|
||||
repo::add_new_character(conn, user_id, &character_id, &character_data, book_id, lang, series_character_id, last_update)?;
|
||||
|
||||
let attribute_arrays: Vec<(&str, &Vec<AttributeName>)> = vec![
|
||||
("physical", &character.physical),
|
||||
("psychological", &character.psychological),
|
||||
("relations", &character.relations),
|
||||
("skills", &character.skills),
|
||||
("weaknesses", &character.weaknesses),
|
||||
("strengths", &character.strengths),
|
||||
("goals", &character.goals),
|
||||
("motivations", &character.motivations),
|
||||
("arc", &character.arc),
|
||||
("secrets", &character.secrets),
|
||||
("fears", &character.fears),
|
||||
("flaws", &character.flaws),
|
||||
("beliefs", &character.beliefs),
|
||||
("conflicts", &character.conflicts),
|
||||
("quotes", &character.quotes),
|
||||
("distinguishingMarks", &character.distinguishing_marks),
|
||||
("items", &character.items),
|
||||
("affiliations", &character.affiliations),
|
||||
];
|
||||
|
||||
for (attribute_type, attribute_items) in attribute_arrays {
|
||||
if !attribute_items.is_empty() {
|
||||
for attribute_item in attribute_items {
|
||||
add_new_attribute(conn, &character_id, user_id, attribute_type, &attribute_item.name, lang, None)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(character_id)
|
||||
}
|
||||
|
||||
/// Updates an existing character's core properties.
|
||||
/// Encrypts all updated data before storing in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character` - The character data with updated values
|
||||
/// * `lang` - The language code for localization
|
||||
/// Returns true if the update was successful, false otherwise.
|
||||
pub fn update_character(conn: &Connection, user_id: &str, character: &CharacterPropsPost, lang: Lang) -> AppResult<bool> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
let character_id: &str = match character.id.as_deref() {
|
||||
Some(id) => id,
|
||||
None => return Ok(false),
|
||||
};
|
||||
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
|
||||
let character_data = repo::CharacterData {
|
||||
first_name: encrypt_data_with_user_key(&character.name, &user_key)?,
|
||||
last_name: Some(encrypt_data_with_user_key(&character.last_name, &user_key)?),
|
||||
nickname: Some(encrypt_data_with_user_key(if character.nickname.is_empty() { "" } else { &character.nickname }, &user_key)?),
|
||||
age: if let Some(age_value) = character.age { Some(encrypt_data_with_user_key(&age_value.to_string(), &user_key)?) } else { None },
|
||||
gender: Some(encrypt_data_with_user_key(if character.gender.is_empty() { "" } else { &character.gender }, &user_key)?),
|
||||
species: Some(encrypt_data_with_user_key(if character.species.is_empty() { "" } else { &character.species }, &user_key)?),
|
||||
nationality: Some(encrypt_data_with_user_key(if character.nationality.is_empty() { "" } else { &character.nationality }, &user_key)?),
|
||||
status: Some(encrypt_data_with_user_key(if character.status.is_empty() { "alive" } else { &character.status }, &user_key)?),
|
||||
title: Some(encrypt_data_with_user_key(&character.title, &user_key)?),
|
||||
category: Some(encrypt_data_with_user_key(&character.category, &user_key)?),
|
||||
image: Some(encrypt_data_with_user_key(&character.image, &user_key)?),
|
||||
role: Some(encrypt_data_with_user_key(&character.role, &user_key)?),
|
||||
biography: Some(encrypt_data_with_user_key(character.biography.as_deref().unwrap_or(""), &user_key)?),
|
||||
history: Some(encrypt_data_with_user_key(character.history.as_deref().unwrap_or(""), &user_key)?),
|
||||
speech_pattern: Some(encrypt_data_with_user_key(character.speech_pattern.as_deref().unwrap_or(""), &user_key)?),
|
||||
catchphrase: Some(encrypt_data_with_user_key(character.catchphrase.as_deref().unwrap_or(""), &user_key)?),
|
||||
residence: Some(encrypt_data_with_user_key(character.residence.as_deref().unwrap_or(""), &user_key)?),
|
||||
notes: Some(encrypt_data_with_user_key(character.notes.as_deref().unwrap_or(""), &user_key)?),
|
||||
color: Some(encrypt_data_with_user_key(character.color.as_deref().unwrap_or(""), &user_key)?),
|
||||
};
|
||||
|
||||
let series_character_id: Option<&str> = character.series_character_id.as_deref();
|
||||
repo::update_character(conn, user_id, character_id, &character_data, last_update, lang, series_character_id)
|
||||
}
|
||||
|
||||
/// Adds a new attribute to a character.
|
||||
/// Attributes are categorized properties like physical traits, skills, or goals.
|
||||
/// * `conn` - Database connection
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `r#type` - The type/category of the attribute (e.g., 'physical', 'skills')
|
||||
/// * `name` - The value/name of the attribute
|
||||
/// * `lang` - The language code for localization
|
||||
/// * `existing_attribute_id` - Optional existing attribute ID for updates or imports
|
||||
/// Returns the unique identifier of the newly created attribute.
|
||||
pub fn add_new_attribute(conn: &Connection, character_id: &str, user_id: &str, r#type: &str, name: &str, lang: Lang, existing_attribute_id: Option<&str>) -> AppResult<String> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let attribute_id: String = create_unique_id(existing_attribute_id);
|
||||
let encrypted_type: String = encrypt_data_with_user_key(r#type, &user_key)?;
|
||||
let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?;
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
repo::insert_attribute(conn, &attribute_id, character_id, user_id, &encrypted_type, &encrypted_name, last_update, lang)
|
||||
}
|
||||
|
||||
/// Deletes an attribute from a character.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `attribute_id` - The unique identifier of the attribute to delete
|
||||
/// * `deleted_at` - The timestamp of deletion
|
||||
/// * `lang` - The language code for localization
|
||||
/// Returns true if the deletion was successful, false otherwise.
|
||||
pub fn delete_attribute(conn: &Connection, user_id: &str, book_id: &str, attribute_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
|
||||
let deleted: bool = repo::delete_attribute(conn, user_id, attribute_id, lang)?;
|
||||
if deleted {
|
||||
let removal_id: String = create_unique_id(None);
|
||||
tombstone_repo::insert(conn, &removal_id, "book_characters_attributes", attribute_id, Some(book_id), user_id, deleted_at, lang)?;
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
/// Deletes a character and all its related data.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `character_id` - The unique identifier of the character to delete
|
||||
/// * `deleted_at` - The timestamp of deletion
|
||||
/// * `lang` - The language code for localization
|
||||
/// Returns true if the deletion was successful.
|
||||
pub fn delete_character(conn: &Connection, user_id: &str, book_id: &str, character_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
|
||||
let deleted: bool = repo::delete_character(conn, user_id, character_id, lang)?;
|
||||
if deleted {
|
||||
let removal_id: String = create_unique_id(None);
|
||||
tombstone_repo::insert(conn, &removal_id, "book_characters", character_id, Some(book_id), user_id, deleted_at, lang)?;
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
/// Retrieves all attributes for a specific character, grouped by type.
|
||||
/// Decrypts attribute data using the user's encryption key.
|
||||
/// * `conn` - Database connection
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `lang` - The language code for localization
|
||||
/// Returns an array of character attributes grouped by type.
|
||||
pub fn get_attributes(conn: &Connection, character_id: &str, user_id: &str, lang: Lang) -> AppResult<Vec<CharacterAttribute>> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let encrypted_attributes: Vec<repo::AttributeResult> = repo::fetch_attributes(conn, character_id, user_id, lang)?;
|
||||
|
||||
if encrypted_attributes.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut attributes_by_type: HashMap<String, Vec<Attribute>> = HashMap::new();
|
||||
|
||||
for encrypted_attribute in encrypted_attributes {
|
||||
let decrypted_type: String = decrypt_data_with_user_key(&encrypted_attribute.attribute_name, &user_key)?;
|
||||
let decrypted_value: String = if encrypted_attribute.attribute_value.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_attribute.attribute_value, &user_key)? };
|
||||
|
||||
attributes_by_type.entry(decrypted_type).or_insert_with(Vec::new).push(Attribute {
|
||||
id: encrypted_attribute.attr_id,
|
||||
name: decrypted_value,
|
||||
});
|
||||
}
|
||||
|
||||
let character_attributes: Vec<CharacterAttribute> = attributes_by_type.into_iter().map(|(attribute_type, values)| CharacterAttribute {
|
||||
r#type: attribute_type,
|
||||
values,
|
||||
}).collect();
|
||||
|
||||
Ok(character_attributes)
|
||||
}
|
||||
|
||||
/// Retrieves complete character data including all attributes for multiple characters.
|
||||
/// Used for exporting or displaying full character profiles.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `characters` - An array of character IDs to retrieve
|
||||
/// * `lang` - The language code for localization
|
||||
/// Returns an array of complete character objects with all their attributes.
|
||||
pub fn get_complete_character_list(conn: &Connection, user_id: &str, book_id: &str, characters: &[String], lang: Lang) -> AppResult<Vec<CompleteCharacterProps>> {
|
||||
let encrypted_character_list: Vec<repo::CompleteCharacterResult> = match repo::fetch_complete_characters(conn, user_id, book_id, characters, lang) {
|
||||
Ok(result) => result,
|
||||
Err(_) => return Ok(vec![]),
|
||||
};
|
||||
|
||||
if encrypted_character_list.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let mut complete_characters_map: HashMap<String, CompleteCharacterProps> = HashMap::new();
|
||||
|
||||
for encrypted_character in &encrypted_character_list {
|
||||
if encrypted_character.character_id.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !complete_characters_map.contains_key(&encrypted_character.character_id) {
|
||||
let decrypted_character = CompleteCharacterProps {
|
||||
id: None,
|
||||
name: if encrypted_character.first_name.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.first_name, &user_key)? },
|
||||
last_name: if encrypted_character.last_name.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.last_name, &user_key)? },
|
||||
nickname: if let Some(ref value) = encrypted_character.nickname { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
age: if let Some(ref value) = encrypted_character.age { Some(decrypt_data_with_user_key(value, &user_key)?.parse::<i64>().unwrap_or(0)) } else { None },
|
||||
gender: if let Some(ref value) = encrypted_character.gender { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
species: if let Some(ref value) = encrypted_character.species { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
nationality: if let Some(ref value) = encrypted_character.nationality { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
status: if let Some(ref value) = encrypted_character.status { decrypt_data_with_user_key(value, &user_key)? } else { "alive".to_string() },
|
||||
title: if encrypted_character.title.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.title, &user_key)? },
|
||||
category: if encrypted_character.category.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.category, &user_key)? },
|
||||
image: None,
|
||||
role: if encrypted_character.role.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.role, &user_key)? },
|
||||
biography: if encrypted_character.biography.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.biography, &user_key)? },
|
||||
history: if encrypted_character.history.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.history, &user_key)? },
|
||||
speech_pattern: if let Some(ref value) = encrypted_character.speech_pattern { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
catchphrase: if let Some(ref value) = encrypted_character.catchphrase { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
residence: if let Some(ref value) = encrypted_character.residence { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
notes: if let Some(ref value) = encrypted_character.notes { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
color: if let Some(ref value) = encrypted_character.color { decrypt_data_with_user_key(value, &user_key)? } else { String::new() },
|
||||
physical: vec![],
|
||||
psychological: vec![],
|
||||
relations: vec![],
|
||||
skills: vec![],
|
||||
weaknesses: vec![],
|
||||
strengths: vec![],
|
||||
goals: vec![],
|
||||
motivations: vec![],
|
||||
arc: vec![],
|
||||
secrets: vec![],
|
||||
fears: vec![],
|
||||
flaws: vec![],
|
||||
beliefs: vec![],
|
||||
conflicts: vec![],
|
||||
quotes: vec![],
|
||||
distinguishing_marks: vec![],
|
||||
items: vec![],
|
||||
affiliations: vec![],
|
||||
};
|
||||
complete_characters_map.insert(encrypted_character.character_id.clone(), decrypted_character);
|
||||
}
|
||||
|
||||
let character_entry: &mut CompleteCharacterProps = match complete_characters_map.get_mut(&encrypted_character.character_id) {
|
||||
Some(entry) => entry,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if encrypted_character.attribute_name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let decrypted_attribute_name: String = decrypt_data_with_user_key(&encrypted_character.attribute_name, &user_key)?;
|
||||
let decrypted_attribute_value: String = if encrypted_character.attribute_value.is_empty() { String::new() } else { decrypt_data_with_user_key(&encrypted_character.attribute_value, &user_key)? };
|
||||
|
||||
let attribute = Attribute { id: String::new(), name: decrypted_attribute_value };
|
||||
|
||||
match decrypted_attribute_name.as_str() {
|
||||
"physical" => character_entry.physical.push(attribute),
|
||||
"psychological" => character_entry.psychological.push(attribute),
|
||||
"relations" => character_entry.relations.push(attribute),
|
||||
"skills" => character_entry.skills.push(attribute),
|
||||
"weaknesses" => character_entry.weaknesses.push(attribute),
|
||||
"strengths" => character_entry.strengths.push(attribute),
|
||||
"goals" => character_entry.goals.push(attribute),
|
||||
"motivations" => character_entry.motivations.push(attribute),
|
||||
"arc" => character_entry.arc.push(attribute),
|
||||
"secrets" => character_entry.secrets.push(attribute),
|
||||
"fears" => character_entry.fears.push(attribute),
|
||||
"flaws" => character_entry.flaws.push(attribute),
|
||||
"beliefs" => character_entry.beliefs.push(attribute),
|
||||
"conflicts" => character_entry.conflicts.push(attribute),
|
||||
"quotes" => character_entry.quotes.push(attribute),
|
||||
"distinguishingMarks" => character_entry.distinguishing_marks.push(attribute),
|
||||
"items" => character_entry.items.push(attribute),
|
||||
"affiliations" => character_entry.affiliations.push(attribute),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(complete_characters_map.into_values().collect())
|
||||
}
|
||||
|
||||
/// Generates a formatted vCard-style string representation of characters.
|
||||
/// Useful for AI context or text-based exports.
|
||||
/// * `characters` - An array of complete character objects to format
|
||||
/// Returns a formatted string containing all character information.
|
||||
pub fn character_v_card(characters: &[CompleteCharacterProps]) -> String {
|
||||
let mut unique_characters_map: HashMap<String, CompleteCharacterProps> = HashMap::new();
|
||||
|
||||
for character in characters {
|
||||
let character_identifier: String = if !character.name.is_empty() {
|
||||
character.name.clone()
|
||||
} else if let Some(ref id) = character.id {
|
||||
id.clone()
|
||||
} else {
|
||||
"unknown".to_string()
|
||||
};
|
||||
|
||||
if !unique_characters_map.contains_key(&character_identifier) {
|
||||
unique_characters_map.insert(character_identifier.clone(), CompleteCharacterProps {
|
||||
id: None,
|
||||
name: character.name.clone(),
|
||||
last_name: character.last_name.clone(),
|
||||
nickname: character.nickname.clone(),
|
||||
age: character.age,
|
||||
gender: character.gender.clone(),
|
||||
species: character.species.clone(),
|
||||
nationality: character.nationality.clone(),
|
||||
status: character.status.clone(),
|
||||
title: character.title.clone(),
|
||||
category: character.category.clone(),
|
||||
image: None,
|
||||
role: character.role.clone(),
|
||||
biography: character.biography.clone(),
|
||||
history: character.history.clone(),
|
||||
speech_pattern: character.speech_pattern.clone(),
|
||||
catchphrase: character.catchphrase.clone(),
|
||||
residence: character.residence.clone(),
|
||||
notes: character.notes.clone(),
|
||||
color: character.color.clone(),
|
||||
physical: vec![],
|
||||
psychological: vec![],
|
||||
relations: vec![],
|
||||
skills: vec![],
|
||||
weaknesses: vec![],
|
||||
strengths: vec![],
|
||||
goals: vec![],
|
||||
motivations: vec![],
|
||||
arc: vec![],
|
||||
secrets: vec![],
|
||||
fears: vec![],
|
||||
flaws: vec![],
|
||||
beliefs: vec![],
|
||||
conflicts: vec![],
|
||||
quotes: vec![],
|
||||
distinguishing_marks: vec![],
|
||||
items: vec![],
|
||||
affiliations: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
let aggregated_character_data: &mut CompleteCharacterProps = unique_characters_map.get_mut(&character_identifier).unwrap();
|
||||
|
||||
for attribute in &character.physical { aggregated_character_data.physical.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.psychological { aggregated_character_data.psychological.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.relations { aggregated_character_data.relations.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.skills { aggregated_character_data.skills.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.weaknesses { aggregated_character_data.weaknesses.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.strengths { aggregated_character_data.strengths.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.goals { aggregated_character_data.goals.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.motivations { aggregated_character_data.motivations.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.arc { aggregated_character_data.arc.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.secrets { aggregated_character_data.secrets.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.fears { aggregated_character_data.fears.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.flaws { aggregated_character_data.flaws.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.beliefs { aggregated_character_data.beliefs.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.conflicts { aggregated_character_data.conflicts.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.quotes { aggregated_character_data.quotes.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.distinguishing_marks { aggregated_character_data.distinguishing_marks.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.items { aggregated_character_data.items.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
for attribute in &character.affiliations { aggregated_character_data.affiliations.push(Attribute { id: attribute.id.clone(), name: attribute.name.clone() }); }
|
||||
}
|
||||
|
||||
let formatted_characters_description: String = unique_characters_map.values().map(|character| {
|
||||
let mut character_description_lines: Vec<String> = Vec::new();
|
||||
|
||||
let full_name: String = [&character.name, &character.last_name].iter().filter(|name| !name.is_empty()).map(|name| name.as_str()).collect::<Vec<&str>>().join(" ");
|
||||
if !full_name.is_empty() {
|
||||
character_description_lines.push(format!("Nom : {}", full_name));
|
||||
}
|
||||
|
||||
let simple_properties: Vec<(&str, &str)> = vec![
|
||||
("Category", &character.category),
|
||||
("Title", &character.title),
|
||||
("Role", &character.role),
|
||||
("Biography", &character.biography),
|
||||
("History", &character.history),
|
||||
];
|
||||
for (property_label, property_value) in simple_properties {
|
||||
if !property_value.is_empty() {
|
||||
character_description_lines.push(format!("{} : {}", property_label, property_value));
|
||||
}
|
||||
}
|
||||
|
||||
let array_properties: Vec<(&str, &Vec<Attribute>)> = vec![
|
||||
("Physical", &character.physical),
|
||||
("Psychological", &character.psychological),
|
||||
("Relations", &character.relations),
|
||||
("Skills", &character.skills),
|
||||
("Weaknesses", &character.weaknesses),
|
||||
("Strengths", &character.strengths),
|
||||
("Goals", &character.goals),
|
||||
("Motivations", &character.motivations),
|
||||
("Arc", &character.arc),
|
||||
("Secrets", &character.secrets),
|
||||
("Fears", &character.fears),
|
||||
("Flaws", &character.flaws),
|
||||
("Beliefs", &character.beliefs),
|
||||
("Conflicts", &character.conflicts),
|
||||
("Quotes", &character.quotes),
|
||||
("DistinguishingMarks", &character.distinguishing_marks),
|
||||
("Items", &character.items),
|
||||
("Affiliations", &character.affiliations),
|
||||
];
|
||||
for (capitalized_property_key, attribute_values) in array_properties {
|
||||
if !attribute_values.is_empty() {
|
||||
let formatted_attribute_values: String = attribute_values.iter().map(|attribute_item| attribute_item.name.as_str()).collect::<Vec<&str>>().join(", ");
|
||||
character_description_lines.push(format!("{} : {}", capitalized_property_key, formatted_attribute_values));
|
||||
}
|
||||
}
|
||||
|
||||
character_description_lines.join("\n")
|
||||
}).collect::<Vec<String>>().join("\n\n");
|
||||
|
||||
formatted_characters_description
|
||||
}
|
||||
1
src-tauri/src/domains/cover/mod.rs
Normal file
1
src-tauri/src/domains/cover/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod service;
|
||||
53
src-tauri/src/domains/cover/service.rs
Normal file
53
src-tauri/src/domains/cover/service.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
|
||||
use rusqlite::Connection;
|
||||
|
||||
use crate::crypto::encryption::decrypt_data_with_user_key;
|
||||
use crate::crypto::key_manager::get_user_encryption_key;
|
||||
use crate::domains::book::repo;
|
||||
use crate::error::AppResult;
|
||||
use crate::helpers::timestamp_in_seconds;
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
/// Retrieves and decrypts the cover picture for a specific book.
|
||||
/// Returns the decrypted cover image data, or an empty string if not found.
|
||||
pub fn get_cover_picture(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<String> {
|
||||
let cover_query: repo::BookCoverQuery = repo::fetch_book_cover(conn, user_id, book_id, lang)?;
|
||||
if !cover_query.cover_image.is_empty() {
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
decrypt_data_with_user_key(&cover_query.cover_image, &user_encryption_key)
|
||||
} else {
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes the cover picture association for a specific book.
|
||||
/// Clears the cover image reference in the database.
|
||||
/// Returns true if the cover was successfully deleted, false otherwise.
|
||||
pub fn delete_cover_picture(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let _existing_cover_name: String = get_cover_picture(conn, user_id, book_id, lang)?;
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
repo::update_book_cover(conn, book_id, "", user_id, last_update, lang)
|
||||
}
|
||||
|
||||
/// Retrieves and decrypts a picture file, returning it as a base64-encoded string.
|
||||
/// Returns the base64-encoded image data, or an empty string if the image cannot be read.
|
||||
pub fn get_picture(_user_id: &str, user_key: &str, image: &str, _lang: Lang) -> String {
|
||||
if image.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
match try_get_picture(user_key, image) {
|
||||
Ok(base64_data) => base64_data,
|
||||
Err(_) => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn try_get_picture(user_key: &str, image: &str) -> AppResult<String> {
|
||||
let decrypted_file_name: String = decrypt_data_with_user_key(image, user_key)?;
|
||||
let user_directory: &Path = Path::new(&decrypted_file_name);
|
||||
let file_data: Vec<u8> = fs::read(user_directory)
|
||||
.map_err(|error| crate::error::AppError::Internal(error.to_string()))?;
|
||||
Ok(BASE64.encode(&file_data))
|
||||
}
|
||||
1
src-tauri/src/domains/download/mod.rs
Normal file
1
src-tauri/src/domains/download/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod service;
|
||||
211
src-tauri/src/domains/download/service.rs
Normal file
211
src-tauri/src/domains/download/service.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use rusqlite::Connection;
|
||||
|
||||
use crate::crypto::encryption::encrypt_data_with_user_key;
|
||||
use crate::crypto::key_manager::get_user_encryption_key;
|
||||
use crate::domains::act::repo as act_repo;
|
||||
use crate::domains::book::repo as book_repo;
|
||||
use crate::domains::book::service::CompleteBook;
|
||||
use crate::domains::chapter::repo as chapter_repo;
|
||||
use crate::domains::character::repo as character_repo;
|
||||
use crate::domains::character::repo::SyncCharacterData;
|
||||
use crate::domains::guideline::repo as guideline_repo;
|
||||
use crate::domains::incident::repo as incident_repo;
|
||||
use crate::domains::issue::repo as issue_repo;
|
||||
use crate::domains::location::repo as location_repo;
|
||||
use crate::domains::plotpoint::repo as plotpoint_repo;
|
||||
use crate::domains::spell::repo as spell_repo;
|
||||
use crate::domains::spell_tag::repo as spell_tag_repo;
|
||||
use crate::domains::chapter_content::repo as chapter_content_repo;
|
||||
use crate::domains::world::repo as world_repo;
|
||||
use crate::error::AppResult;
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
/// Saves a complete book with all its associated data to the local database.
|
||||
/// This method encrypts all sensitive data using the user's encryption key before storing.
|
||||
/// It processes and inserts all book components including chapters, incidents, plot points,
|
||||
/// chapter contents, chapter infos, characters, character attributes, locations, location elements,
|
||||
/// location sub-elements, worlds, world elements, act summaries, AI guidelines, guidelines, and issues.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user who owns the book
|
||||
/// * `data` - The complete book data structure containing all book components to save
|
||||
/// * `lang` - The language code for localization
|
||||
/// Returns true if all data was saved successfully, false otherwise.
|
||||
pub fn save_complete_book(conn: &Connection, user_id: &str, data: &CompleteBook, lang: Lang) -> AppResult<bool> {
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
let book_data = &data.erit_books[0];
|
||||
let encrypted_book_title: String = encrypt_data_with_user_key(&book_data.title, &user_encryption_key)?;
|
||||
let encrypted_book_sub_title: Option<String> = if let Some(ref sub_title) = book_data.sub_title { Some(encrypt_data_with_user_key(sub_title, &user_encryption_key)?) } else { None };
|
||||
let encrypted_book_summary: Option<String> = if let Some(ref summary) = book_data.summary { Some(encrypt_data_with_user_key(summary, &user_encryption_key)?) } else { None };
|
||||
let encrypted_book_cover_image: Option<String> = if let Some(ref cover_image) = book_data.cover_image { Some(encrypt_data_with_user_key(cover_image, &user_encryption_key)?) } else { None };
|
||||
|
||||
let book_inserted: bool = book_repo::insert_sync_book(conn, &book_data.book_id, user_id, &book_data.book_type, &encrypted_book_title, &book_data.hashed_title, encrypted_book_sub_title.as_deref(), book_data.hashed_sub_title.as_deref(), encrypted_book_summary.as_deref(), book_data.serie_id, book_data.desired_release_date.as_deref(), book_data.desired_word_count, book_data.words_count, encrypted_book_cover_image.as_deref(), book_data.last_update, lang)?;
|
||||
if !book_inserted { return Ok(false); }
|
||||
|
||||
for chapter in &data.chapters {
|
||||
let encrypted_chapter_title: String = encrypt_data_with_user_key(&chapter.title, &user_encryption_key)?;
|
||||
let chapter_inserted: bool = chapter_repo::insert_sync_chapter(conn, &chapter.chapter_id, &chapter.book_id, user_id, &encrypted_chapter_title, Some(&chapter.hashed_title), None, Some(chapter.chapter_order), chapter.last_update, lang)?;
|
||||
if !chapter_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for incident in &data.incidents {
|
||||
let encrypted_incident_title: String = encrypt_data_with_user_key(&incident.name, &user_encryption_key)?;
|
||||
let encrypted_incident_summary: Option<String> = if let Some(ref description) = incident.description { Some(encrypt_data_with_user_key(description, &user_encryption_key)?) } else { None };
|
||||
let incident_inserted: bool = incident_repo::insert_sync_incident(conn, &incident.incident_id, user_id, &incident.chapter_id, &encrypted_incident_title, &incident.hashed_name, encrypted_incident_summary.as_deref(), incident.last_update, lang)?;
|
||||
if !incident_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for plot_point in &data.plot_points {
|
||||
let encrypted_plot_point_title: String = encrypt_data_with_user_key(&plot_point.name, &user_encryption_key)?;
|
||||
let encrypted_plot_point_summary: Option<String> = if let Some(ref description) = plot_point.description { Some(encrypt_data_with_user_key(description, &user_encryption_key)?) } else { None };
|
||||
let plot_point_inserted: bool = plotpoint_repo::insert_sync_plot_point(conn, &plot_point.plot_point_id, &encrypted_plot_point_title, &plot_point.hashed_name, encrypted_plot_point_summary.as_deref(), None, user_id, &plot_point.chapter_id, plot_point.last_update, lang)?;
|
||||
if !plot_point_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for chapter_content in &data.chapter_contents {
|
||||
let encrypted_chapter_content: Option<String> = if let Some(ref content) = chapter_content.content { Some(encrypt_data_with_user_key(content, &user_encryption_key)?) } else { None };
|
||||
let chapter_content_inserted: bool = chapter_content_repo::insert_sync_chapter_content(conn, &chapter_content.content_id, &chapter_content.chapter_id, user_id, chapter_content.version, encrypted_chapter_content.as_deref(), 0, 0, chapter_content.last_update, lang)?;
|
||||
if !chapter_content_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for chapter_info in &data.chapter_infos {
|
||||
let encrypted_chapter_summary: Option<String> = if let Some(ref summary) = chapter_info.summary { Some(encrypt_data_with_user_key(summary, &user_encryption_key)?) } else { None };
|
||||
let encrypted_chapter_goal: Option<String> = if let Some(ref notes) = chapter_info.notes { Some(encrypt_data_with_user_key(notes, &user_encryption_key)?) } else { None };
|
||||
let chapter_info_inserted: bool = chapter_repo::insert_sync_chapter_info(conn, &chapter_info.chapter_id, &chapter_info.chapter_id, None, None, None, &chapter_info.chapter_id, user_id, encrypted_chapter_summary.as_deref(), encrypted_chapter_goal.as_deref(), chapter_info.last_update, lang)?;
|
||||
if !chapter_info_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for character in &data.characters {
|
||||
let character_data = SyncCharacterData {
|
||||
first_name: encrypt_data_with_user_key(&character.first_name, &user_encryption_key)?,
|
||||
last_name: if let Some(ref last_name) = character.last_name { Some(encrypt_data_with_user_key(last_name, &user_encryption_key)?) } else { None },
|
||||
nickname: if let Some(ref nickname) = character.nickname { Some(encrypt_data_with_user_key(nickname, &user_encryption_key)?) } else { None },
|
||||
age: if let Some(ref age) = character.age { Some(encrypt_data_with_user_key(&age.to_string(), &user_encryption_key)?) } else { None },
|
||||
gender: if let Some(ref gender) = character.gender { Some(encrypt_data_with_user_key(gender, &user_encryption_key)?) } else { None },
|
||||
species: if let Some(ref species) = character.species { Some(encrypt_data_with_user_key(species, &user_encryption_key)?) } else { None },
|
||||
nationality: if let Some(ref nationality) = character.nationality { Some(encrypt_data_with_user_key(nationality, &user_encryption_key)?) } else { None },
|
||||
status: if let Some(ref status) = character.status { Some(encrypt_data_with_user_key(status, &user_encryption_key)?) } else { None },
|
||||
category: encrypt_data_with_user_key(&character.category, &user_encryption_key)?,
|
||||
title: if let Some(ref title) = character.title { Some(encrypt_data_with_user_key(title, &user_encryption_key)?) } else { None },
|
||||
image: if let Some(ref image) = character.image { Some(encrypt_data_with_user_key(image, &user_encryption_key)?) } else { None },
|
||||
role: if let Some(ref role) = character.role { Some(encrypt_data_with_user_key(role, &user_encryption_key)?) } else { None },
|
||||
biography: if let Some(ref biography) = character.biography { Some(encrypt_data_with_user_key(biography, &user_encryption_key)?) } else { None },
|
||||
history: if let Some(ref history) = character.history { Some(encrypt_data_with_user_key(history, &user_encryption_key)?) } else { None },
|
||||
speech_pattern: if let Some(ref speech_pattern) = character.speech_pattern { Some(encrypt_data_with_user_key(speech_pattern, &user_encryption_key)?) } else { None },
|
||||
catchphrase: if let Some(ref catchphrase) = character.catchphrase { Some(encrypt_data_with_user_key(catchphrase, &user_encryption_key)?) } else { None },
|
||||
residence: if let Some(ref residence) = character.residence { Some(encrypt_data_with_user_key(residence, &user_encryption_key)?) } else { None },
|
||||
notes: if let Some(ref notes) = character.notes { Some(encrypt_data_with_user_key(notes, &user_encryption_key)?) } else { None },
|
||||
color: if let Some(ref color) = character.color { Some(encrypt_data_with_user_key(color, &user_encryption_key)?) } else { None },
|
||||
};
|
||||
let character_inserted: bool = character_repo::insert_sync_character(conn, &character.character_id, &character.book_id, user_id, &character_data, character.last_update, lang)?;
|
||||
if !character_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for character_attribute in &data.character_attributes {
|
||||
let encrypted_attribute_name: String = encrypt_data_with_user_key(&character_attribute.attribute_name, &user_encryption_key)?;
|
||||
let encrypted_attribute_value: String = encrypt_data_with_user_key(&character_attribute.attribute_value, &user_encryption_key)?;
|
||||
let character_attribute_inserted: bool = character_repo::insert_sync_character_attribute(conn, &character_attribute.attr_id, &character_attribute.character_id, user_id, &encrypted_attribute_name, &encrypted_attribute_value, character_attribute.last_update, lang)?;
|
||||
if !character_attribute_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for location in &data.locations {
|
||||
let encrypted_location_name: String = encrypt_data_with_user_key(&location.loc_name, &user_encryption_key)?;
|
||||
let location_inserted: bool = location_repo::insert_sync_location(conn, &location.loc_id, &location.book_id, user_id, &encrypted_location_name, &location.loc_original_name, location.last_update, lang)?;
|
||||
if !location_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for location_element in &data.location_elements {
|
||||
let encrypted_location_element_name: String = encrypt_data_with_user_key(&location_element.element_name, &user_encryption_key)?;
|
||||
let encrypted_location_element_description: Option<String> = if let Some(ref element_description) = location_element.element_description { Some(encrypt_data_with_user_key(element_description, &user_encryption_key)?) } else { None };
|
||||
let location_element_inserted: bool = location_repo::insert_sync_location_element(conn, &location_element.element_id, &location_element.location_id, user_id, &encrypted_location_element_name, &location_element.original_name, encrypted_location_element_description.as_deref(), location_element.last_update, lang)?;
|
||||
if !location_element_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for location_sub_element in &data.location_sub_elements {
|
||||
let encrypted_sub_element_name: String = encrypt_data_with_user_key(&location_sub_element.sub_elem_name, &user_encryption_key)?;
|
||||
let encrypted_sub_element_description: Option<String> = if let Some(ref sub_elem_description) = location_sub_element.sub_elem_description { Some(encrypt_data_with_user_key(sub_elem_description, &user_encryption_key)?) } else { None };
|
||||
let location_sub_element_inserted: bool = location_repo::insert_sync_location_sub_element(conn, &location_sub_element.sub_element_id, &location_sub_element.element_id, user_id, &encrypted_sub_element_name, &location_sub_element.original_name, encrypted_sub_element_description.as_deref(), location_sub_element.last_update, lang)?;
|
||||
if !location_sub_element_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for world in &data.worlds {
|
||||
let encrypted_world_name: String = encrypt_data_with_user_key(&world.name, &user_encryption_key)?;
|
||||
let encrypted_world_history: Option<String> = if let Some(ref history) = world.history { Some(encrypt_data_with_user_key(history, &user_encryption_key)?) } else { None };
|
||||
let encrypted_world_politics: Option<String> = if let Some(ref politics) = world.politics { Some(encrypt_data_with_user_key(politics, &user_encryption_key)?) } else { None };
|
||||
let encrypted_world_economy: Option<String> = if let Some(ref economy) = world.economy { Some(encrypt_data_with_user_key(economy, &user_encryption_key)?) } else { None };
|
||||
let encrypted_world_religion: Option<String> = if let Some(ref religion) = world.religion { Some(encrypt_data_with_user_key(religion, &user_encryption_key)?) } else { None };
|
||||
let encrypted_world_languages: Option<String> = if let Some(ref languages) = world.languages { Some(encrypt_data_with_user_key(languages, &user_encryption_key)?) } else { None };
|
||||
let world_inserted: bool = world_repo::insert_sync_world(conn, &world.world_id, &encrypted_world_name, &world.hashed_name, user_id, &world.book_id, encrypted_world_history.as_deref(), encrypted_world_politics.as_deref(), encrypted_world_economy.as_deref(), encrypted_world_religion.as_deref(), encrypted_world_languages.as_deref(), world.last_update, lang)?;
|
||||
if !world_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for world_element in &data.world_elements {
|
||||
let encrypted_world_element_name: String = encrypt_data_with_user_key(&world_element.name, &user_encryption_key)?;
|
||||
let encrypted_world_element_description: Option<String> = if let Some(ref description) = world_element.description { Some(encrypt_data_with_user_key(description, &user_encryption_key)?) } else { None };
|
||||
let world_element_inserted: bool = world_repo::insert_sync_world_element(conn, &world_element.element_id, &world_element.world_id, user_id, world_element.element_type, &encrypted_world_element_name, &world_element.original_name, encrypted_world_element_description.as_deref(), world_element.last_update, lang)?;
|
||||
if !world_element_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for act_summary in &data.act_summaries {
|
||||
let encrypted_act_summary: String = encrypt_data_with_user_key(&act_summary.summary, &user_encryption_key)?;
|
||||
let act_summary_inserted: bool = act_repo::insert_sync_act_summary(conn, &act_summary.summary_id, &act_summary.book_id, user_id, act_summary.act_number, Some(&encrypted_act_summary), act_summary.last_update, lang)?;
|
||||
if !act_summary_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for ai_guideline in &data.ai_guide_line {
|
||||
let encrypted_global_resume: Option<String> = if let Some(ref global_resume) = ai_guideline.global_resume { Some(encrypt_data_with_user_key(global_resume, &user_encryption_key)?) } else { None };
|
||||
let encrypted_themes: Option<String> = if let Some(ref themes) = ai_guideline.themes { Some(encrypt_data_with_user_key(themes, &user_encryption_key)?) } else { None };
|
||||
let encrypted_tone: Option<String> = if let Some(ref tone) = ai_guideline.tone { Some(encrypt_data_with_user_key(tone, &user_encryption_key)?) } else { None };
|
||||
let encrypted_atmosphere: Option<String> = if let Some(ref atmosphere) = ai_guideline.atmosphere { Some(encrypt_data_with_user_key(atmosphere, &user_encryption_key)?) } else { None };
|
||||
let encrypted_current_resume: Option<String> = if let Some(ref current_resume) = ai_guideline.current_resume { Some(encrypt_data_with_user_key(current_resume, &user_encryption_key)?) } else { None };
|
||||
let ai_guideline_inserted: bool = guideline_repo::insert_sync_ai_guide_line(conn, user_id, &ai_guideline.book_id, encrypted_global_resume.as_deref(), encrypted_themes.as_deref(), ai_guideline.verbe_tense, ai_guideline.narrative_type, ai_guideline.langue, ai_guideline.dialogue_type, encrypted_tone.as_deref(), encrypted_atmosphere.as_deref(), encrypted_current_resume.as_deref(), ai_guideline.last_update, lang)?;
|
||||
if !ai_guideline_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for guideline in &data.guide_line {
|
||||
let encrypted_tone: Option<String> = if let Some(ref tone) = guideline.tone { Some(encrypt_data_with_user_key(tone, &user_encryption_key)?) } else { None };
|
||||
let encrypted_atmosphere: Option<String> = if let Some(ref atmosphere) = guideline.atmosphere { Some(encrypt_data_with_user_key(atmosphere, &user_encryption_key)?) } else { None };
|
||||
let encrypted_writing_style: Option<String> = if let Some(ref writing_style) = guideline.writing_style { Some(encrypt_data_with_user_key(writing_style, &user_encryption_key)?) } else { None };
|
||||
let encrypted_themes: Option<String> = if let Some(ref themes) = guideline.themes { Some(encrypt_data_with_user_key(themes, &user_encryption_key)?) } else { None };
|
||||
let encrypted_symbolism: Option<String> = if let Some(ref symbolism) = guideline.symbolism { Some(encrypt_data_with_user_key(symbolism, &user_encryption_key)?) } else { None };
|
||||
let encrypted_motifs: Option<String> = if let Some(ref motifs) = guideline.motifs { Some(encrypt_data_with_user_key(motifs, &user_encryption_key)?) } else { None };
|
||||
let encrypted_narrative_voice: Option<String> = if let Some(ref narrative_voice) = guideline.narrative_voice { Some(encrypt_data_with_user_key(narrative_voice, &user_encryption_key)?) } else { None };
|
||||
let encrypted_pacing: Option<String> = if let Some(ref pacing) = guideline.pacing { Some(encrypt_data_with_user_key(pacing, &user_encryption_key)?) } else { None };
|
||||
let encrypted_intended_audience: Option<String> = if let Some(ref intended_audience) = guideline.intended_audience { Some(encrypt_data_with_user_key(intended_audience, &user_encryption_key)?) } else { None };
|
||||
let encrypted_key_messages: Option<String> = if let Some(ref key_messages) = guideline.key_messages { Some(encrypt_data_with_user_key(key_messages, &user_encryption_key)?) } else { None };
|
||||
let guideline_inserted: bool = guideline_repo::insert_sync_guide_line(conn, user_id, &guideline.book_id, encrypted_tone.as_deref(), encrypted_atmosphere.as_deref(), encrypted_writing_style.as_deref(), encrypted_themes.as_deref(), encrypted_symbolism.as_deref(), encrypted_motifs.as_deref(), encrypted_narrative_voice.as_deref(), encrypted_pacing.as_deref(), encrypted_intended_audience.as_deref(), encrypted_key_messages.as_deref(), guideline.last_update, lang)?;
|
||||
if !guideline_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for issue in &data.issues {
|
||||
let encrypted_issue_name: String = encrypt_data_with_user_key(&issue.name, &user_encryption_key)?;
|
||||
let issue_inserted: bool = issue_repo::insert_sync_issue(conn, &issue.issue_id, user_id, &issue.chapter_id, &encrypted_issue_name, &issue.hashed_name, issue.last_update, lang)?;
|
||||
if !issue_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for book_tool in &data.book_tools {
|
||||
let book_tool_inserted: bool = book_repo::insert_sync_book_tools(conn, &book_tool.book_id, user_id, book_tool.characters_enabled, book_tool.worlds_enabled, book_tool.locations_enabled, book_tool.spells_enabled, book_tool.last_update, lang);
|
||||
if !book_tool_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for spell_tag in &data.spell_tags {
|
||||
let encrypted_tag_name: String = encrypt_data_with_user_key(&spell_tag.name, &user_encryption_key)?;
|
||||
let spell_tag_inserted: bool = spell_tag_repo::insert_sync_spell_tag(conn, &spell_tag.tag_id, &spell_tag.book_id, user_id, &encrypted_tag_name, &spell_tag.hashed_name, spell_tag.color.as_deref(), spell_tag.last_update, lang)?;
|
||||
if !spell_tag_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
for spell in &data.spells {
|
||||
let encrypted_name: String = encrypt_data_with_user_key(&spell.name, &user_encryption_key)?;
|
||||
let encrypted_description: String = encrypt_data_with_user_key(&spell.description, &user_encryption_key)?;
|
||||
let encrypted_appearance: String = encrypt_data_with_user_key(&spell.appearance, &user_encryption_key)?;
|
||||
let encrypted_tags: String = encrypt_data_with_user_key(&spell.tags, &user_encryption_key)?;
|
||||
let encrypted_power_level: Option<String> = if let Some(ref power_level) = spell.power_level { Some(encrypt_data_with_user_key(power_level, &user_encryption_key)?) } else { None };
|
||||
let encrypted_components: Option<String> = if let Some(ref components) = spell.components { Some(encrypt_data_with_user_key(components, &user_encryption_key)?) } else { None };
|
||||
let encrypted_limitations: Option<String> = if let Some(ref limitations) = spell.limitations { Some(encrypt_data_with_user_key(limitations, &user_encryption_key)?) } else { None };
|
||||
let encrypted_notes: Option<String> = if let Some(ref notes) = spell.notes { Some(encrypt_data_with_user_key(notes, &user_encryption_key)?) } else { None };
|
||||
let spell_inserted: bool = spell_repo::insert_sync_spell(conn, &spell.spell_id, &spell.book_id, user_id, &encrypted_name, &spell.name_hash, Some(&encrypted_description), Some(&encrypted_appearance), Some(&encrypted_tags), encrypted_power_level.as_deref(), encrypted_components.as_deref(), encrypted_limitations.as_deref(), encrypted_notes.as_deref(), spell.last_update, lang)?;
|
||||
if !spell_inserted { return Ok(false); }
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
1
src-tauri/src/domains/export/mod.rs
Normal file
1
src-tauri/src/domains/export/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod service;
|
||||
251
src-tauri/src/domains/export/service.rs
Normal file
251
src-tauri/src/domains/export/service.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use docx_rs::{
|
||||
AlignmentType as DocxAlignmentType, Docx, Paragraph as DocxParagraph, Run,
|
||||
};
|
||||
use epub_builder::{EpubBuilder, EpubContent, ReferenceType, ZipLibrary};
|
||||
use printpdf::{Mm, PdfDocument};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::domains::book::service::{CompleteBookData, CompleteChapterContent};
|
||||
use crate::domains::chapter::service::{get_chapters_or_sheet, tip_tap_to_html, ChapterContentData};
|
||||
use crate::error::{AppError, AppResult};
|
||||
|
||||
pub const MAIN_STYLE: &str = r#"h1 {
|
||||
font-size: 24px !important;
|
||||
font-weight: bold !important;
|
||||
text-indent: 24px !important;
|
||||
}
|
||||
p {
|
||||
text-indent: 30px !important;
|
||||
margin-top: 0.7em !important;
|
||||
margin-bottom: 0.7em !important;
|
||||
text-align: justify !important;
|
||||
}"#;
|
||||
|
||||
pub struct ExportResult {
|
||||
pub buffer: Vec<u8>,
|
||||
pub file_name: String,
|
||||
}
|
||||
|
||||
/// Transforms book data into a DOCX document.
|
||||
/// * `book_data` - The complete book data to export
|
||||
/// Returns the DOCX buffer and filename.
|
||||
pub fn transform_to_docx(book_data: &CompleteBookData) -> AppResult<ExportResult> {
|
||||
let book_title: &str = &book_data.title;
|
||||
let filename: String = format!("{}.docx", book_title);
|
||||
|
||||
let mut docx: Docx = Docx::new();
|
||||
|
||||
docx = docx.add_paragraph(
|
||||
DocxParagraph::new()
|
||||
.add_run(Run::new().add_text(book_title).bold().size(48))
|
||||
.align(DocxAlignmentType::Center)
|
||||
);
|
||||
|
||||
if !book_data.sub_title.is_empty() {
|
||||
docx = docx.add_paragraph(
|
||||
DocxParagraph::new()
|
||||
.add_run(Run::new().add_text(&book_data.sub_title).italic().size(32))
|
||||
.align(DocxAlignmentType::Center)
|
||||
);
|
||||
}
|
||||
|
||||
if !book_data.summary.is_empty() {
|
||||
docx = docx.add_paragraph(
|
||||
DocxParagraph::new()
|
||||
.add_run(Run::new().add_text(&book_data.summary).italic().size(24))
|
||||
.align(DocxAlignmentType::Both)
|
||||
);
|
||||
}
|
||||
|
||||
let chapters: Vec<ChapterContentData> = get_chapters_or_sheet(&book_data.chapters);
|
||||
|
||||
for chapter in &chapters {
|
||||
if chapter.content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
docx = docx.add_paragraph(
|
||||
DocxParagraph::new()
|
||||
.add_run(Run::new().add_text(&chapter.title).bold().size(32))
|
||||
.align(DocxAlignmentType::Center)
|
||||
.page_break_before(true)
|
||||
);
|
||||
|
||||
let paragraphs: Vec<&str> = chapter.content.split('\n').collect();
|
||||
|
||||
for paragraph in paragraphs {
|
||||
if paragraph.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
docx = docx.add_paragraph(
|
||||
DocxParagraph::new()
|
||||
.add_run(Run::new().add_text(paragraph).size(24))
|
||||
.align(DocxAlignmentType::Both)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut buffer: Vec<u8> = Vec::new();
|
||||
docx.build()
|
||||
.pack(&mut Cursor::new(&mut buffer))
|
||||
.map_err(|error| AppError::Internal(format!("DOCX generation failed: {}", error)))?;
|
||||
|
||||
Ok(ExportResult { buffer, file_name: filename })
|
||||
}
|
||||
|
||||
/// Transforms book data into a PDF document.
|
||||
/// * `book_data` - The complete book data to export
|
||||
/// Returns the PDF buffer and filename.
|
||||
pub fn transform_to_pdf(book_data: &CompleteBookData) -> AppResult<ExportResult> {
|
||||
let book_title: &str = &book_data.title;
|
||||
let filename: String = format!("{}.pdf", book_title);
|
||||
|
||||
let (pdf_document, page_index, layer_index) = PdfDocument::new(book_title, Mm(210.0), Mm(297.0), "Title Page");
|
||||
let font = pdf_document
|
||||
.add_builtin_font(printpdf::BuiltinFont::Helvetica)
|
||||
.map_err(|error| AppError::Internal(format!("PDF font error: {}", error)))?;
|
||||
|
||||
let current_layer = pdf_document.get_page(page_index).get_layer(layer_index);
|
||||
let mut current_y: f32 = 270.0;
|
||||
|
||||
current_layer.use_text(book_title, 20.0, Mm(20.0), Mm(current_y), &font);
|
||||
current_y -= 10.0;
|
||||
|
||||
if !book_data.sub_title.is_empty() && book_data.sub_title.trim() != "" {
|
||||
current_layer.use_text(&book_data.sub_title, 16.0, Mm(20.0), Mm(current_y), &font);
|
||||
current_y -= 10.0;
|
||||
}
|
||||
|
||||
if !book_data.summary.is_empty() && book_data.summary.trim() != "" {
|
||||
current_layer.use_text(&book_data.summary, 12.0, Mm(20.0), Mm(current_y), &font);
|
||||
}
|
||||
|
||||
let chapters: Vec<ChapterContentData> = get_chapters_or_sheet(&book_data.chapters);
|
||||
|
||||
for chapter in &chapters {
|
||||
if chapter.content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (new_page_index, new_layer_index) = pdf_document.add_page(Mm(210.0), Mm(297.0), &chapter.title);
|
||||
let chapter_layer = pdf_document.get_page(new_page_index).get_layer(new_layer_index);
|
||||
let mut chapter_y: f32 = 270.0;
|
||||
|
||||
chapter_layer.use_text(&chapter.title, 16.0, Mm(20.0), Mm(chapter_y), &font);
|
||||
chapter_y -= 10.0;
|
||||
|
||||
let lines: Vec<&str> = chapter.content.split('\n').collect();
|
||||
for line in lines {
|
||||
if chapter_y < 20.0 {
|
||||
break;
|
||||
}
|
||||
chapter_layer.use_text(line, 12.0, Mm(20.0), Mm(chapter_y), &font);
|
||||
chapter_y -= 6.0;
|
||||
}
|
||||
}
|
||||
|
||||
let buffer: Vec<u8> = pdf_document
|
||||
.save_to_bytes()
|
||||
.map_err(|error| AppError::Internal(format!("PDF generation failed: {}", error)))?;
|
||||
|
||||
Ok(ExportResult { buffer, file_name: filename })
|
||||
}
|
||||
|
||||
/// Transforms book data into an EPUB document.
|
||||
/// * `book_data` - The complete book data to export
|
||||
/// Returns the EPUB buffer and filename.
|
||||
pub fn transform_to_epub(book_data: &CompleteBookData) -> AppResult<ExportResult> {
|
||||
let book_title: &str = &book_data.title;
|
||||
let book_id: &str = &book_data.book_id;
|
||||
|
||||
let zip_library = ZipLibrary::new().map_err(|error| AppError::Internal(format!("EPUB zip error: {}", error)))?;
|
||||
let mut epub_builder: EpubBuilder<ZipLibrary> = EpubBuilder::new(zip_library)
|
||||
.map_err(|error| AppError::Internal(format!("EPUB builder error: {}", error)))?;
|
||||
|
||||
let full_title: String = if book_data.sub_title.is_empty() {
|
||||
book_title.to_string()
|
||||
} else {
|
||||
format!("{} - {}", book_title, &book_data.sub_title)
|
||||
};
|
||||
|
||||
epub_builder
|
||||
.metadata("title", &full_title)
|
||||
.map_err(|error| AppError::Internal(format!("EPUB metadata error: {}", error)))?;
|
||||
epub_builder
|
||||
.metadata("language", "fr")
|
||||
.map_err(|error| AppError::Internal(format!("EPUB metadata error: {}", error)))?;
|
||||
epub_builder
|
||||
.metadata("identifier", &format!("urn:uuid:{}", book_id))
|
||||
.map_err(|error| AppError::Internal(format!("EPUB metadata error: {}", error)))?;
|
||||
epub_builder
|
||||
.metadata("author", &format!("{} {}", &book_data.user_infos.first_name, &book_data.user_infos.last_name))
|
||||
.map_err(|error| AppError::Internal(format!("EPUB metadata error: {}", error)))?;
|
||||
epub_builder
|
||||
.metadata("publisher", "ERitors Scribe")
|
||||
.map_err(|error| AppError::Internal(format!("EPUB metadata error: {}", error)))?;
|
||||
|
||||
epub_builder
|
||||
.stylesheet(MAIN_STYLE.as_bytes())
|
||||
.map_err(|error| AppError::Internal(format!("EPUB stylesheet error: {}", error)))?;
|
||||
|
||||
let has_regular_chapters: bool = book_data.chapters.iter().any(|chapter| chapter.order > 0);
|
||||
let chapters_to_export: Vec<&CompleteChapterContent> = if has_regular_chapters {
|
||||
book_data.chapters.iter().filter(|chapter| chapter.order > 0).collect()
|
||||
} else {
|
||||
book_data.chapters.iter().filter(|chapter| chapter.order == -1).collect()
|
||||
};
|
||||
|
||||
for chapter in &chapters_to_export {
|
||||
if chapter.content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let chapter_index: String = format!("chapter{}", chapter.order);
|
||||
let parsed_content: Value = serde_json::from_str(&chapter.content)
|
||||
.map_err(|error| AppError::Internal(format!("JSON parse error: {}", error)))?;
|
||||
let html_content: String = tip_tap_to_html(&parsed_content);
|
||||
|
||||
let xhtml_page: String = format!(
|
||||
r#"<?xml version="1.0" encoding="utf-8"?>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<title>{}</title>
|
||||
<link rel="stylesheet" type="text/css" href="styles.css"/>
|
||||
</head>
|
||||
<body>
|
||||
{}
|
||||
</body>
|
||||
</html>"#,
|
||||
&chapter.title, html_content
|
||||
);
|
||||
|
||||
epub_builder
|
||||
.add_content(
|
||||
EpubContent::new(format!("{}.xhtml", chapter_index), xhtml_page.as_bytes())
|
||||
.title(&chapter.title)
|
||||
.reftype(ReferenceType::Text),
|
||||
)
|
||||
.map_err(|error| AppError::Internal(format!("EPUB content error: {}", error)))?;
|
||||
}
|
||||
|
||||
if !book_data.cover_image.is_empty() {
|
||||
let image_buffer: Vec<u8> = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD,
|
||||
&book_data.cover_image,
|
||||
)
|
||||
.map_err(|error| AppError::Internal(format!("Base64 decode error: {}", error)))?;
|
||||
|
||||
epub_builder
|
||||
.add_cover_image("cover.jpg", image_buffer.as_slice(), "image/jpeg")
|
||||
.map_err(|error| AppError::Internal(format!("EPUB cover error: {}", error)))?;
|
||||
}
|
||||
|
||||
let mut epub_buffer: Vec<u8> = Vec::new();
|
||||
epub_builder
|
||||
.generate(&mut epub_buffer)
|
||||
.map_err(|error| AppError::Internal(format!("EPUB generation failed: {}", error)))?;
|
||||
|
||||
Ok(ExportResult { buffer: epub_buffer, file_name: format!("{}.epub", book_title) })
|
||||
}
|
||||
2
src-tauri/src/domains/guideline/mod.rs
Normal file
2
src-tauri/src/domains/guideline/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod repo;
|
||||
pub mod service;
|
||||
435
src-tauri/src/domains/guideline/repo.rs
Normal file
435
src-tauri/src/domains/guideline/repo.rs
Normal file
@@ -0,0 +1,435 @@
|
||||
use rusqlite::{params, Connection};
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
pub struct BookAIGuideLineTable {
|
||||
pub user_id: String,
|
||||
pub book_id: String,
|
||||
pub global_resume: Option<String>,
|
||||
pub themes: Option<String>,
|
||||
pub verbe_tense: Option<i64>,
|
||||
pub narrative_type: Option<i64>,
|
||||
pub langue: Option<i64>,
|
||||
pub dialogue_type: Option<i64>,
|
||||
pub tone: Option<String>,
|
||||
pub atmosphere: Option<String>,
|
||||
pub current_resume: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct BookGuideLineTable {
|
||||
pub user_id: String,
|
||||
pub book_id: String,
|
||||
pub tone: Option<String>,
|
||||
pub atmosphere: Option<String>,
|
||||
pub writing_style: Option<String>,
|
||||
pub themes: Option<String>,
|
||||
pub symbolism: Option<String>,
|
||||
pub motifs: Option<String>,
|
||||
pub narrative_voice: Option<String>,
|
||||
pub pacing: Option<String>,
|
||||
pub intended_audience: Option<String>,
|
||||
pub key_messages: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedGuideLineResult {
|
||||
pub book_id: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedAIGuideLineResult {
|
||||
pub book_id: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct GuideLineQuery {
|
||||
pub tone: String,
|
||||
pub atmosphere: String,
|
||||
pub writing_style: String,
|
||||
pub themes: String,
|
||||
pub symbolism: String,
|
||||
pub motifs: String,
|
||||
pub narrative_voice: String,
|
||||
pub pacing: String,
|
||||
pub intended_audience: String,
|
||||
pub key_messages: String,
|
||||
}
|
||||
|
||||
pub struct GuideLineAIQuery {
|
||||
pub user_id: String,
|
||||
pub book_id: String,
|
||||
pub global_resume: Option<String>,
|
||||
pub themes: Option<String>,
|
||||
pub verbe_tense: Option<i64>,
|
||||
pub narrative_type: Option<i64>,
|
||||
pub langue: Option<i64>,
|
||||
pub dialogue_type: Option<i64>,
|
||||
pub tone: Option<String>,
|
||||
pub atmosphere: Option<String>,
|
||||
pub current_resume: Option<String>,
|
||||
pub meta: String,
|
||||
}
|
||||
|
||||
/// Fetches the guideline for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of guideline query results.
|
||||
/// Errors if the guideline cannot be retrieved.
|
||||
pub fn fetch_guide_line(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<GuideLineQuery>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT tone, atmosphere, writing_style, themes, symbolism, motifs, narrative_voice, pacing, intended_audience, key_messages FROM book_guide_line WHERE book_id=?1 AND user_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la ligne directrice.".to_string() } else { "Unable to retrieve guideline.".to_string() }))?;
|
||||
|
||||
let guidelines = statement
|
||||
.query_map(params![book_id, user_id], |query_row| {
|
||||
Ok(GuideLineQuery {
|
||||
tone: query_row.get(0)?, atmosphere: query_row.get(1)?,
|
||||
writing_style: query_row.get(2)?, themes: query_row.get(3)?,
|
||||
symbolism: query_row.get(4)?, motifs: query_row.get(5)?,
|
||||
narrative_voice: query_row.get(6)?, pacing: query_row.get(7)?,
|
||||
intended_audience: query_row.get(8)?, key_messages: query_row.get(9)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la ligne directrice.".to_string() } else { "Unable to retrieve guideline.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la ligne directrice.".to_string() } else { "Unable to retrieve guideline.".to_string() }))?;
|
||||
|
||||
Ok(guidelines)
|
||||
}
|
||||
|
||||
/// Updates or inserts a guideline for a specific book.
|
||||
/// If the guideline exists, it updates it; otherwise, it inserts a new one.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `encrypted_tone` - The encrypted tone value
|
||||
/// * `encrypted_atmosphere` - The encrypted atmosphere value
|
||||
/// * `encrypted_writing_style` - The encrypted writing style value
|
||||
/// * `encrypted_themes` - The encrypted themes value
|
||||
/// * `encrypted_symbolism` - The encrypted symbolism value
|
||||
/// * `encrypted_motifs` - The encrypted motifs value
|
||||
/// * `encrypted_narrative_voice` - The encrypted narrative voice value
|
||||
/// * `encrypted_pacing` - The encrypted pacing value
|
||||
/// * `encrypted_key_messages` - The encrypted key messages value
|
||||
/// * `encrypted_intended_audience` - The encrypted intended audience value
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the operation was successful.
|
||||
/// Errors if the guideline cannot be updated or inserted.
|
||||
pub fn update_guide_line(
|
||||
conn: &Connection, user_id: &str, book_id: &str, encrypted_tone: Option<&str>,
|
||||
encrypted_atmosphere: Option<&str>, encrypted_writing_style: Option<&str>,
|
||||
encrypted_themes: Option<&str>, encrypted_symbolism: Option<&str>,
|
||||
encrypted_motifs: Option<&str>, encrypted_narrative_voice: Option<&str>,
|
||||
encrypted_pacing: Option<&str>, encrypted_key_messages: Option<&str>,
|
||||
encrypted_intended_audience: Option<&str>, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute(
|
||||
"UPDATE book_guide_line SET tone=?1, atmosphere=?2, writing_style=?3, themes=?4, symbolism=?5, motifs=?6, narrative_voice=?7, pacing=?8, intended_audience=?9, key_messages=?10, last_update=?11 WHERE user_id=?12 AND book_id=?13",
|
||||
params![encrypted_tone, encrypted_atmosphere, encrypted_writing_style, encrypted_themes, encrypted_symbolism, encrypted_motifs, encrypted_narrative_voice, encrypted_pacing, encrypted_intended_audience, encrypted_key_messages, last_update, user_id, book_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour la ligne directrice.".to_string() } else { "Unable to update guideline.".to_string() }))?;
|
||||
|
||||
if update_result > 0 {
|
||||
Ok(true)
|
||||
} else {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO book_guide_line (user_id, book_id, tone, atmosphere, writing_style, themes, symbolism, motifs, narrative_voice, pacing, intended_audience, key_messages, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13)",
|
||||
params![user_id, book_id, encrypted_tone, encrypted_atmosphere, encrypted_writing_style, encrypted_themes, encrypted_symbolism, encrypted_motifs, encrypted_narrative_voice, encrypted_pacing, encrypted_intended_audience, encrypted_key_messages, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour la ligne directrice.".to_string() } else { "Unable to update guideline.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts or updates an AI guideline for a specific book.
|
||||
/// If the AI guideline exists, it updates it; otherwise, it inserts a new one.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `narrative_type` - The narrative type identifier
|
||||
/// * `dialogue_type` - The dialogue type identifier
|
||||
/// * `encrypted_plot_summary` - The encrypted plot summary
|
||||
/// * `encrypted_tone_atmosphere` - The encrypted tone and atmosphere value
|
||||
/// * `verb_tense` - The verb tense identifier
|
||||
/// * `language` - The language identifier
|
||||
/// * `encrypted_themes` - The encrypted themes value
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the operation was successful.
|
||||
/// Errors if the AI guideline cannot be inserted or updated.
|
||||
pub fn insert_ai_guide_line(
|
||||
conn: &Connection, user_id: &str, book_id: &str, narrative_type: Option<i64>,
|
||||
dialogue_type: Option<i64>, encrypted_plot_summary: Option<&str>,
|
||||
encrypted_tone_atmosphere: Option<&str>, verb_tense: Option<i64>,
|
||||
language: Option<i64>, encrypted_themes: Option<&str>, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute(
|
||||
"UPDATE book_ai_guide_line SET narrative_type=?1, dialogue_type=?2, global_resume=?3, atmosphere=?4, verbe_tense=?5, langue=?6, themes=?7, last_update=?8 WHERE user_id=?9 AND book_id=?10",
|
||||
params![narrative_type, dialogue_type, encrypted_plot_summary, encrypted_tone_atmosphere, verb_tense, language, encrypted_themes, last_update, user_id, book_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer la ligne directrice IA.".to_string() } else { "Unable to insert AI guideline.".to_string() }))?;
|
||||
|
||||
if update_result > 0 {
|
||||
Ok(true)
|
||||
} else {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO book_ai_guide_line (user_id, book_id, global_resume, themes, verbe_tense, narrative_type, langue, dialogue_type, tone, atmosphere, current_resume, last_update) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)",
|
||||
params![user_id, book_id, encrypted_plot_summary, encrypted_themes, verb_tense, narrative_type, language, dialogue_type, encrypted_tone_atmosphere, encrypted_tone_atmosphere, encrypted_plot_summary, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer la ligne directrice IA.".to_string() } else { "Unable to insert AI guideline.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches the AI guideline for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the AI guideline query result.
|
||||
/// Errors if the AI guideline cannot be retrieved or is not found.
|
||||
pub fn fetch_guide_line_ai(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<GuideLineAIQuery> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT narrative_type, dialogue_type, global_resume, atmosphere, verbe_tense, langue, themes, current_resume FROM book_ai_guide_line WHERE user_id=?1 AND book_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la ligne directrice IA.".to_string() } else { "Unable to retrieve AI guideline.".to_string() }))?;
|
||||
|
||||
let ai_guideline = statement
|
||||
.query_row(params![user_id, book_id], |query_row| {
|
||||
Ok(GuideLineAIQuery {
|
||||
narrative_type: query_row.get(0)?, dialogue_type: query_row.get(1)?,
|
||||
global_resume: query_row.get(2)?, atmosphere: query_row.get(3)?,
|
||||
verbe_tense: query_row.get(4)?, langue: query_row.get(5)?,
|
||||
themes: query_row.get(6)?, current_resume: query_row.get(7)?,
|
||||
user_id: user_id.to_string(), book_id: book_id.to_string(),
|
||||
tone: None, meta: String::new(),
|
||||
})
|
||||
})
|
||||
.map_err(|error| match error {
|
||||
rusqlite::Error::QueryReturnedNoRows => AppError::NotFound(if lang == Lang::Fr { "Ligne directrice IA non trouvée.".to_string() } else { "AI guideline not found.".to_string() }),
|
||||
_ => AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la ligne directrice IA.".to_string() } else { "Unable to retrieve AI guideline.".to_string() }),
|
||||
})?;
|
||||
|
||||
Ok(ai_guideline)
|
||||
}
|
||||
|
||||
/// Fetches the book AI guideline table data for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of book AI guideline table entries.
|
||||
/// Errors if the AI guideline cannot be retrieved.
|
||||
pub fn fetch_book_ai_guide_line(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookAIGuideLineTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT user_id, book_id, global_resume, themes, verbe_tense, narrative_type, langue, dialogue_type, tone, atmosphere, current_resume, last_update FROM book_ai_guide_line WHERE user_id=?1 AND book_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la ligne directrice IA.".to_string() } else { "Unable to retrieve AI guideline.".to_string() }))?;
|
||||
|
||||
let ai_guidelines = statement
|
||||
.query_map(params![user_id, book_id], |query_row| {
|
||||
Ok(BookAIGuideLineTable {
|
||||
user_id: query_row.get(0)?, book_id: query_row.get(1)?,
|
||||
global_resume: query_row.get(2)?, themes: query_row.get(3)?,
|
||||
verbe_tense: query_row.get(4)?, narrative_type: query_row.get(5)?,
|
||||
langue: query_row.get(6)?, dialogue_type: query_row.get(7)?,
|
||||
tone: query_row.get(8)?, atmosphere: query_row.get(9)?,
|
||||
current_resume: query_row.get(10)?, last_update: query_row.get(11)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la ligne directrice IA.".to_string() } else { "Unable to retrieve AI guideline.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la ligne directrice IA.".to_string() } else { "Unable to retrieve AI guideline.".to_string() }))?;
|
||||
|
||||
Ok(ai_guidelines)
|
||||
}
|
||||
|
||||
/// Fetches the book guideline table data for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of book guideline table entries.
|
||||
/// Errors if the guideline cannot be retrieved.
|
||||
pub fn fetch_book_guide_line_table(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookGuideLineTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT user_id, book_id, tone, atmosphere, writing_style, themes, symbolism, motifs, narrative_voice, pacing, intended_audience, key_messages, last_update FROM book_guide_line WHERE user_id=?1 AND book_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la ligne directrice.".to_string() } else { "Unable to retrieve guideline.".to_string() }))?;
|
||||
|
||||
let guidelines = statement
|
||||
.query_map(params![user_id, book_id], |query_row| {
|
||||
Ok(BookGuideLineTable {
|
||||
user_id: query_row.get(0)?, book_id: query_row.get(1)?,
|
||||
tone: query_row.get(2)?, atmosphere: query_row.get(3)?,
|
||||
writing_style: query_row.get(4)?, themes: query_row.get(5)?,
|
||||
symbolism: query_row.get(6)?, motifs: query_row.get(7)?,
|
||||
narrative_voice: query_row.get(8)?, pacing: query_row.get(9)?,
|
||||
intended_audience: query_row.get(10)?, key_messages: query_row.get(11)?,
|
||||
last_update: query_row.get(12)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la ligne directrice.".to_string() } else { "Unable to retrieve guideline.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la ligne directrice.".to_string() } else { "Unable to retrieve guideline.".to_string() }))?;
|
||||
|
||||
Ok(guidelines)
|
||||
}
|
||||
|
||||
/// Fetches all synced guidelines for a specific user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of synced guideline results containing book_id and last_update.
|
||||
/// Errors if the synced guidelines cannot be retrieved.
|
||||
pub fn fetch_synced_guide_line(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedGuideLineResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT book_id, last_update FROM book_guide_line WHERE user_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lignes directrices synchronisées.".to_string() } else { "Unable to retrieve synced guidelines.".to_string() }))?;
|
||||
|
||||
let synced_guidelines = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedGuideLineResult { book_id: query_row.get(0)?, last_update: query_row.get(1)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lignes directrices synchronisées.".to_string() } else { "Unable to retrieve synced guidelines.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lignes directrices synchronisées.".to_string() } else { "Unable to retrieve synced guidelines.".to_string() }))?;
|
||||
|
||||
Ok(synced_guidelines)
|
||||
}
|
||||
|
||||
/// Fetches all synced AI guidelines for a specific user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of synced AI guideline results containing book_id and last_update.
|
||||
/// Errors if the synced AI guidelines cannot be retrieved.
|
||||
pub fn fetch_synced_ai_guide_line(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedAIGuideLineResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT book_id, last_update FROM book_ai_guide_line WHERE user_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lignes directrices IA synchronisées.".to_string() } else { "Unable to retrieve synced AI guidelines.".to_string() }))?;
|
||||
|
||||
let synced_ai_guidelines = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedAIGuideLineResult { book_id: query_row.get(0)?, last_update: query_row.get(1)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lignes directrices IA synchronisées.".to_string() } else { "Unable to retrieve synced AI guidelines.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lignes directrices IA synchronisées.".to_string() } else { "Unable to retrieve synced AI guidelines.".to_string() }))?;
|
||||
|
||||
Ok(synced_ai_guidelines)
|
||||
}
|
||||
|
||||
/// Checks if a guideline exists for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the guideline exists, false otherwise.
|
||||
/// Errors if the existence check fails.
|
||||
pub fn guide_line_exist(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT 1 FROM book_guide_line WHERE user_id=?1 AND book_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de la ligne directrice.".to_string() } else { "Unable to check guideline existence.".to_string() }))?;
|
||||
|
||||
let exists = statement
|
||||
.query_row(params![user_id, book_id], |_query_row| Ok(true))
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Checks if an AI guideline exists for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the AI guideline exists, false otherwise.
|
||||
/// Errors if the existence check fails.
|
||||
pub fn ai_guide_line_exist(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT 1 FROM book_ai_guide_line WHERE user_id=?1 AND book_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de la ligne directrice IA.".to_string() } else { "Unable to check AI guideline existence.".to_string() }))?;
|
||||
|
||||
let exists = statement
|
||||
.query_row(params![user_id, book_id], |_query_row| Ok(true))
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
/// Inserts a synced AI guideline for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `global_resume` - The global resume value (nullable)
|
||||
/// * `themes` - The themes value (nullable)
|
||||
/// * `verbe_tense` - The verb tense identifier (nullable)
|
||||
/// * `narrative_type` - The narrative type identifier (nullable)
|
||||
/// * `langue` - The language identifier (nullable)
|
||||
/// * `dialogue_type` - The dialogue type identifier (nullable)
|
||||
/// * `tone` - The tone value (nullable)
|
||||
/// * `atmosphere` - The atmosphere value (nullable)
|
||||
/// * `current_resume` - The current resume value (nullable)
|
||||
/// * `last_update` - The last update timestamp
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the insertion was successful.
|
||||
/// Errors if the AI guideline cannot be inserted.
|
||||
pub fn insert_sync_ai_guide_line(
|
||||
conn: &Connection, user_id: &str, book_id: &str, global_resume: Option<&str>,
|
||||
themes: Option<&str>, verbe_tense: Option<i64>, narrative_type: Option<i64>,
|
||||
langue: Option<i64>, dialogue_type: Option<i64>, tone: Option<&str>,
|
||||
atmosphere: Option<&str>, current_resume: Option<&str>, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO book_ai_guide_line (user_id, book_id, global_resume, themes, verbe_tense, narrative_type, langue, dialogue_type, tone, atmosphere, current_resume, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)",
|
||||
params![user_id, book_id, global_resume, themes, verbe_tense, narrative_type, langue, dialogue_type, tone, atmosphere, current_resume, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer la ligne directrice IA.".to_string() } else { "Unable to insert AI guideline.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Inserts a synced guideline for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user identifier
|
||||
/// * `book_id` - The book identifier
|
||||
/// * `tone` - The tone value (nullable)
|
||||
/// * `atmosphere` - The atmosphere value (nullable)
|
||||
/// * `writing_style` - The writing style value (nullable)
|
||||
/// * `themes` - The themes value (nullable)
|
||||
/// * `symbolism` - The symbolism value (nullable)
|
||||
/// * `motifs` - The motifs value (nullable)
|
||||
/// * `narrative_voice` - The narrative voice value (nullable)
|
||||
/// * `pacing` - The pacing value (nullable)
|
||||
/// * `intended_audience` - The intended audience value (nullable)
|
||||
/// * `key_messages` - The key messages value (nullable)
|
||||
/// * `last_update` - The last update timestamp
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the insertion was successful.
|
||||
/// Errors if the guideline cannot be inserted.
|
||||
pub fn insert_sync_guide_line(
|
||||
conn: &Connection, user_id: &str, book_id: &str, tone: Option<&str>,
|
||||
atmosphere: Option<&str>, writing_style: Option<&str>, themes: Option<&str>,
|
||||
symbolism: Option<&str>, motifs: Option<&str>, narrative_voice: Option<&str>,
|
||||
pacing: Option<&str>, intended_audience: Option<&str>, key_messages: Option<&str>,
|
||||
last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO book_guide_line (user_id, book_id, tone, atmosphere, writing_style, themes, symbolism, motifs, narrative_voice, pacing, intended_audience, key_messages, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
|
||||
params![user_id, book_id, tone, atmosphere, writing_style, themes, symbolism, motifs, narrative_voice, pacing, intended_audience, key_messages, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer la ligne directrice.".to_string() } else { "Unable to insert guideline.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
208
src-tauri/src/domains/guideline/service.rs
Normal file
208
src-tauri/src/domains/guideline/service.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::crypto::encryption::{encrypt_data_with_user_key, decrypt_data_with_user_key};
|
||||
use crate::crypto::key_manager::get_user_encryption_key;
|
||||
use crate::domains::guideline::repo;
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::helpers::timestamp_in_seconds;
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
/// Represents the synced guideline data for a book.
|
||||
pub struct SyncedGuideLine {
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
/// Represents the synced AI guideline data for a book.
|
||||
pub struct SyncedAIGuideLine {
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
/// Represents the decrypted guideline properties for a book.
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GuideLineProps {
|
||||
pub tone: String,
|
||||
pub atmosphere: String,
|
||||
pub writing_style: String,
|
||||
pub themes: String,
|
||||
pub symbolism: String,
|
||||
pub motifs: String,
|
||||
pub narrative_voice: String,
|
||||
pub pacing: String,
|
||||
pub intended_audience: String,
|
||||
pub key_messages: String,
|
||||
}
|
||||
|
||||
/// Represents the decrypted AI guideline data for a book.
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GuideLineAI {
|
||||
pub narrative_type: Option<i64>,
|
||||
pub dialogue_type: Option<i64>,
|
||||
pub global_resume: Option<String>,
|
||||
pub atmosphere: Option<String>,
|
||||
pub verbe_tense: Option<i64>,
|
||||
pub langue: Option<i64>,
|
||||
pub current_resume: Option<String>,
|
||||
pub themes: Option<String>,
|
||||
}
|
||||
|
||||
/// Retrieves and decrypts the guideline for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the decrypted guideline properties or None if not found.
|
||||
pub fn get_guide_line(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Option<GuideLineProps>> {
|
||||
let guide_line_results: Vec<repo::GuideLineQuery> = repo::fetch_guide_line(conn, user_id, book_id, lang)?;
|
||||
|
||||
if guide_line_results.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let guide_line_data: &repo::GuideLineQuery = &guide_line_results[0];
|
||||
let encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
Ok(Some(GuideLineProps {
|
||||
tone: if guide_line_data.tone.is_empty() { String::new() } else { decrypt_data_with_user_key(&guide_line_data.tone, &encryption_key)? },
|
||||
atmosphere: if guide_line_data.atmosphere.is_empty() { String::new() } else { decrypt_data_with_user_key(&guide_line_data.atmosphere, &encryption_key)? },
|
||||
writing_style: if guide_line_data.writing_style.is_empty() { String::new() } else { decrypt_data_with_user_key(&guide_line_data.writing_style, &encryption_key)? },
|
||||
themes: if guide_line_data.themes.is_empty() { String::new() } else { decrypt_data_with_user_key(&guide_line_data.themes, &encryption_key)? },
|
||||
symbolism: if guide_line_data.symbolism.is_empty() { String::new() } else { decrypt_data_with_user_key(&guide_line_data.symbolism, &encryption_key)? },
|
||||
motifs: if guide_line_data.motifs.is_empty() { String::new() } else { decrypt_data_with_user_key(&guide_line_data.motifs, &encryption_key)? },
|
||||
narrative_voice: if guide_line_data.narrative_voice.is_empty() { String::new() } else { decrypt_data_with_user_key(&guide_line_data.narrative_voice, &encryption_key)? },
|
||||
pacing: if guide_line_data.pacing.is_empty() { String::new() } else { decrypt_data_with_user_key(&guide_line_data.pacing, &encryption_key)? },
|
||||
intended_audience: if guide_line_data.intended_audience.is_empty() { String::new() } else { decrypt_data_with_user_key(&guide_line_data.intended_audience, &encryption_key)? },
|
||||
key_messages: if guide_line_data.key_messages.is_empty() { String::new() } else { decrypt_data_with_user_key(&guide_line_data.key_messages, &encryption_key)? },
|
||||
}))
|
||||
}
|
||||
|
||||
/// Updates or creates a guideline for a specific book with encrypted data.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `tone` - The tone setting for the book (nullable)
|
||||
/// * `atmosphere` - The atmosphere setting for the book (nullable)
|
||||
/// * `writing_style` - The writing style for the book (nullable)
|
||||
/// * `themes` - The themes for the book (nullable)
|
||||
/// * `symbolism` - The symbolism elements for the book (nullable)
|
||||
/// * `motifs` - The motifs for the book (nullable)
|
||||
/// * `narrative_voice` - The narrative voice for the book (nullable)
|
||||
/// * `pacing` - The pacing setting for the book (nullable)
|
||||
/// * `key_messages` - The key messages for the book (nullable)
|
||||
/// * `intended_audience` - The intended audience for the book (nullable)
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the update was successful, false otherwise.
|
||||
pub fn update_guide_line(
|
||||
conn: &Connection, user_id: &str, book_id: &str, tone: Option<&str>,
|
||||
atmosphere: Option<&str>, writing_style: Option<&str>, themes: Option<&str>,
|
||||
symbolism: Option<&str>, motifs: Option<&str>, narrative_voice: Option<&str>,
|
||||
pacing: Option<&str>, key_messages: Option<&str>, intended_audience: Option<&str>,
|
||||
lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
let encrypted_tone: String = if let Some(tone_value) = tone { encrypt_data_with_user_key(tone_value, &encryption_key)? } else { String::new() };
|
||||
let encrypted_atmosphere: String = if let Some(atmosphere_value) = atmosphere { encrypt_data_with_user_key(atmosphere_value, &encryption_key)? } else { String::new() };
|
||||
let encrypted_writing_style: String = if let Some(writing_style_value) = writing_style { encrypt_data_with_user_key(writing_style_value, &encryption_key)? } else { String::new() };
|
||||
let encrypted_themes: String = if let Some(themes_value) = themes { encrypt_data_with_user_key(themes_value, &encryption_key)? } else { String::new() };
|
||||
let encrypted_symbolism: String = if let Some(symbolism_value) = symbolism { encrypt_data_with_user_key(symbolism_value, &encryption_key)? } else { String::new() };
|
||||
let encrypted_motifs: String = if let Some(motifs_value) = motifs { encrypt_data_with_user_key(motifs_value, &encryption_key)? } else { String::new() };
|
||||
let encrypted_narrative_voice: String = if let Some(narrative_voice_value) = narrative_voice { encrypt_data_with_user_key(narrative_voice_value, &encryption_key)? } else { String::new() };
|
||||
let encrypted_pacing: String = if let Some(pacing_value) = pacing { encrypt_data_with_user_key(pacing_value, &encryption_key)? } else { String::new() };
|
||||
let encrypted_key_messages: String = if let Some(key_messages_value) = key_messages { encrypt_data_with_user_key(key_messages_value, &encryption_key)? } else { String::new() };
|
||||
let encrypted_intended_audience: String = if let Some(intended_audience_value) = intended_audience { encrypt_data_with_user_key(intended_audience_value, &encryption_key)? } else { String::new() };
|
||||
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
|
||||
repo::update_guide_line(
|
||||
conn, user_id, book_id,
|
||||
Some(&encrypted_tone), Some(&encrypted_atmosphere), Some(&encrypted_writing_style),
|
||||
Some(&encrypted_themes), Some(&encrypted_symbolism), Some(&encrypted_motifs),
|
||||
Some(&encrypted_narrative_voice), Some(&encrypted_pacing),
|
||||
Some(&encrypted_key_messages), Some(&encrypted_intended_audience),
|
||||
last_update, lang,
|
||||
)
|
||||
}
|
||||
|
||||
/// Retrieves and decrypts the AI guideline for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the decrypted AI guideline data with default values if not found.
|
||||
/// Errors if an unexpected error occurs during retrieval.
|
||||
pub fn get_guide_line_ai(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<GuideLineAI> {
|
||||
let encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
match repo::fetch_guide_line_ai(conn, user_id, book_id, lang) {
|
||||
Ok(ai_guide_line_data) => {
|
||||
Ok(GuideLineAI {
|
||||
narrative_type: ai_guide_line_data.narrative_type,
|
||||
dialogue_type: ai_guide_line_data.dialogue_type,
|
||||
global_resume: Some(if let Some(ref global_resume) = ai_guide_line_data.global_resume { decrypt_data_with_user_key(global_resume, &encryption_key)? } else { String::new() }),
|
||||
atmosphere: Some(if let Some(ref atmosphere) = ai_guide_line_data.atmosphere { decrypt_data_with_user_key(atmosphere, &encryption_key)? } else { String::new() }),
|
||||
verbe_tense: ai_guide_line_data.verbe_tense,
|
||||
themes: Some(if let Some(ref themes) = ai_guide_line_data.themes { decrypt_data_with_user_key(themes, &encryption_key)? } else { String::new() }),
|
||||
current_resume: Some(if let Some(ref current_resume) = ai_guide_line_data.current_resume { decrypt_data_with_user_key(current_resume, &encryption_key)? } else { String::new() }),
|
||||
langue: ai_guide_line_data.langue,
|
||||
})
|
||||
}
|
||||
Err(error) => {
|
||||
if error.to_string().contains("not found") || error.to_string().contains("non trouvée") {
|
||||
Ok(GuideLineAI {
|
||||
narrative_type: Some(0),
|
||||
dialogue_type: Some(0),
|
||||
global_resume: Some(String::new()),
|
||||
atmosphere: Some(String::new()),
|
||||
verbe_tense: Some(0),
|
||||
themes: Some(String::new()),
|
||||
current_resume: Some(String::new()),
|
||||
langue: Some(0),
|
||||
})
|
||||
} else {
|
||||
Err(AppError::Internal(if lang == Lang::Fr {
|
||||
"Erreur inconnue lors de la recuperation de la ligne directrice de l'IA.".to_string()
|
||||
} else {
|
||||
"Unknown error while fetching AI guideline.".to_string()
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates or updates an AI guideline for a specific book with encrypted data.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `narrative_type` - The narrative type identifier
|
||||
/// * `dialogue_type` - The dialogue type identifier
|
||||
/// * `plot_summary` - The plot summary text to be encrypted
|
||||
/// * `tone_atmosphere` - The tone and atmosphere description to be encrypted
|
||||
/// * `verb_tense` - The verb tense identifier
|
||||
/// * `language` - The language identifier
|
||||
/// * `themes` - The themes description to be encrypted
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the operation was successful, false otherwise.
|
||||
pub fn set_ai_guide_line(
|
||||
conn: &Connection, user_id: &str, book_id: &str, narrative_type: i64,
|
||||
dialogue_type: i64, plot_summary: &str, tone_atmosphere: &str, verb_tense: i64,
|
||||
language: i64, themes: &str, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
let encrypted_plot_summary: String = if plot_summary.is_empty() { String::new() } else { encrypt_data_with_user_key(plot_summary, &encryption_key)? };
|
||||
let encrypted_tone_atmosphere: String = if tone_atmosphere.is_empty() { String::new() } else { encrypt_data_with_user_key(tone_atmosphere, &encryption_key)? };
|
||||
let encrypted_themes: String = if themes.is_empty() { String::new() } else { encrypt_data_with_user_key(themes, &encryption_key)? };
|
||||
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
|
||||
repo::insert_ai_guide_line(
|
||||
conn, user_id, book_id,
|
||||
Some(narrative_type), Some(dialogue_type),
|
||||
Some(&encrypted_plot_summary), Some(&encrypted_tone_atmosphere),
|
||||
Some(verb_tense), Some(language), Some(&encrypted_themes),
|
||||
last_update, lang,
|
||||
)
|
||||
}
|
||||
2
src-tauri/src/domains/incident/mod.rs
Normal file
2
src-tauri/src/domains/incident/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod repo;
|
||||
pub mod service;
|
||||
248
src-tauri/src/domains/incident/repo.rs
Normal file
248
src-tauri/src/domains/incident/repo.rs
Normal file
@@ -0,0 +1,248 @@
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
pub struct BookIncidentsTable {
|
||||
pub incident_id: String,
|
||||
pub author_id: String,
|
||||
pub book_id: String,
|
||||
pub title: String,
|
||||
pub hashed_title: String,
|
||||
pub summary: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedIncidentResult {
|
||||
pub incident_id: String,
|
||||
pub book_id: String,
|
||||
pub title: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct IncidentQuery {
|
||||
pub incident_id: String,
|
||||
pub title: String,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
/// Fetches all incidents for a specific book belonging to a user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user (author)
|
||||
/// * `book_id` - The ID of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of incidents with their ID, title, and summary.
|
||||
/// Errors if the database query fails.
|
||||
pub fn fetch_all_incitent_incidents(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<IncidentQuery>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT incident_id, title, summary FROM book_incidents WHERE author_id=?1 AND book_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les incidents.".to_string() } else { "Unable to retrieve incidents.".to_string() }))?;
|
||||
|
||||
let incidents = statement
|
||||
.query_map(params![user_id, book_id], |query_row| {
|
||||
Ok(IncidentQuery { incident_id: query_row.get(0)?, title: query_row.get(1)?, summary: query_row.get(2)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les incidents.".to_string() } else { "Unable to retrieve incidents.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les incidents.".to_string() } else { "Unable to retrieve incidents.".to_string() }))?;
|
||||
|
||||
Ok(incidents)
|
||||
}
|
||||
|
||||
/// Inserts a new incident into the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `incident_id` - The unique ID for the new incident
|
||||
/// * `user_id` - The ID of the user (author)
|
||||
/// * `book_id` - The ID of the book
|
||||
/// * `encrypted_name` - The encrypted title of the incident
|
||||
/// * `hashed_name` - The hashed title of the incident
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the incident ID if insertion was successful.
|
||||
/// Errors if the database insertion fails.
|
||||
pub fn insert_new_incident(
|
||||
conn: &Connection, incident_id: &str, user_id: &str, book_id: &str,
|
||||
encrypted_name: &str, hashed_name: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<String> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO book_incidents (incident_id, author_id, book_id, title, hashed_title, last_update) VALUES (?1,?2,?3,?4,?5,?6)",
|
||||
params![incident_id, user_id, book_id, encrypted_name, hashed_name, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter l'élément déclencheur.".to_string() } else { "Unable to add incident.".to_string() }))?;
|
||||
|
||||
if insert_result > 0 {
|
||||
Ok(incident_id.to_string())
|
||||
} else {
|
||||
Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de l'ajout de l'élément déclencheur.".to_string() } else { "Error adding incident.".to_string() }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes an incident from the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user (author)
|
||||
/// * `book_id` - The ID of the book
|
||||
/// * `incident_id` - The ID of the incident to delete
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the incident was deleted, false otherwise.
|
||||
/// Errors if the database deletion fails.
|
||||
pub fn delete_incident(conn: &Connection, user_id: &str, book_id: &str, incident_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let delete_result = conn
|
||||
.execute(
|
||||
"DELETE FROM book_incidents WHERE author_id=?1 AND book_id=?2 AND incident_id=?3",
|
||||
params![user_id, book_id, incident_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer l'élément déclencheur.".to_string() } else { "Unable to delete incident.".to_string() }))?;
|
||||
|
||||
Ok(delete_result > 0)
|
||||
}
|
||||
|
||||
/// Updates an existing incident in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user (author)
|
||||
/// * `book_id` - The ID of the book
|
||||
/// * `incident_id` - The ID of the incident to update
|
||||
/// * `encrypted_incident_name` - The new encrypted title
|
||||
/// * `incident_hashed_name` - The new hashed title
|
||||
/// * `incident_summary` - The new summary
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the incident was updated, false otherwise.
|
||||
/// Errors if the database update fails.
|
||||
pub fn update_incident(
|
||||
conn: &Connection, user_id: &str, book_id: &str, incident_id: &str,
|
||||
encrypted_incident_name: &str, incident_hashed_name: &str, incident_summary: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute(
|
||||
"UPDATE book_incidents SET title=?1, hashed_title=?2, summary=?3, last_update=?4 WHERE author_id=?5 AND book_id=?6 AND incident_id=?7",
|
||||
params![encrypted_incident_name, incident_hashed_name, incident_summary, last_update, user_id, book_id, incident_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour l'incident.".to_string() } else { "Unable to update incident.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Fetches all incidents for a book with complete information.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user (author)
|
||||
/// * `book_id` - The ID of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of complete incident records.
|
||||
/// Errors if the database query fails.
|
||||
pub fn fetch_book_incidents(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookIncidentsTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT incident_id, author_id, book_id, title, hashed_title, summary, last_update FROM book_incidents WHERE author_id=?1 AND book_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les incidents.".to_string() } else { "Unable to retrieve incidents.".to_string() }))?;
|
||||
|
||||
let incidents = statement
|
||||
.query_map(params![user_id, book_id], |query_row| {
|
||||
Ok(BookIncidentsTable {
|
||||
incident_id: query_row.get(0)?, author_id: query_row.get(1)?,
|
||||
book_id: query_row.get(2)?, title: query_row.get(3)?,
|
||||
hashed_title: query_row.get(4)?, summary: query_row.get(5)?,
|
||||
last_update: query_row.get(6)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les incidents.".to_string() } else { "Unable to retrieve incidents.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les incidents.".to_string() } else { "Unable to retrieve incidents.".to_string() }))?;
|
||||
|
||||
Ok(incidents)
|
||||
}
|
||||
|
||||
/// Fetches all synced incidents for a user across all books.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user (author)
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of synced incident records with minimal information.
|
||||
/// Errors if the database query fails.
|
||||
pub fn fetch_synced_incidents(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedIncidentResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT incident_id, book_id, title, last_update FROM book_incidents WHERE author_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les incidents synchronisés.".to_string() } else { "Unable to retrieve synced incidents.".to_string() }))?;
|
||||
|
||||
let synced_incidents = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedIncidentResult { incident_id: query_row.get(0)?, book_id: query_row.get(1)?, title: query_row.get(2)?, last_update: query_row.get(3)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les incidents synchronisés.".to_string() } else { "Unable to retrieve synced incidents.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les incidents synchronisés.".to_string() } else { "Unable to retrieve synced incidents.".to_string() }))?;
|
||||
|
||||
Ok(synced_incidents)
|
||||
}
|
||||
|
||||
/// Inserts a synced incident into the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `incident_id` - The unique ID for the incident
|
||||
/// * `author_id` - The ID of the author
|
||||
/// * `book_id` - The ID of the book
|
||||
/// * `title` - The encrypted title
|
||||
/// * `hashed_title` - The hashed title
|
||||
/// * `summary` - The encrypted summary (can be null)
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the incident was inserted, false otherwise.
|
||||
/// Errors if the database insertion fails.
|
||||
pub fn insert_sync_incident(
|
||||
conn: &Connection, incident_id: &str, author_id: &str, book_id: &str,
|
||||
title: &str, hashed_title: &str, summary: Option<&str>, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO book_incidents (incident_id, author_id, book_id, title, hashed_title, summary, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![incident_id, author_id, book_id, title, hashed_title, summary, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer l'incident.".to_string() } else { "Unable to insert incident.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Fetches complete incident information by its ID.
|
||||
/// * `conn` - Database connection
|
||||
/// * `incident_id` - The ID of the incident to fetch
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array containing the incident record (empty if not found).
|
||||
/// Errors if the database query fails.
|
||||
pub fn fetch_complete_incident_by_id(conn: &Connection, incident_id: &str, lang: Lang) -> AppResult<Vec<BookIncidentsTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT incident_id, author_id, book_id, title, hashed_title, summary, last_update FROM book_incidents WHERE incident_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'incident complet.".to_string() } else { "Unable to retrieve complete incident.".to_string() }))?;
|
||||
|
||||
let incident = statement
|
||||
.query_map(params![incident_id], |query_row| {
|
||||
Ok(BookIncidentsTable {
|
||||
incident_id: query_row.get(0)?, author_id: query_row.get(1)?,
|
||||
book_id: query_row.get(2)?, title: query_row.get(3)?,
|
||||
hashed_title: query_row.get(4)?, summary: query_row.get(5)?,
|
||||
last_update: query_row.get(6)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'incident complet.".to_string() } else { "Unable to retrieve complete incident.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'incident complet.".to_string() } else { "Unable to retrieve complete incident.".to_string() }))?;
|
||||
|
||||
Ok(incident)
|
||||
}
|
||||
|
||||
/// Checks if an incident exists in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user (author)
|
||||
/// * `book_id` - The ID of the book
|
||||
/// * `incident_id` - The ID of the incident to check
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the incident exists, false otherwise.
|
||||
/// Errors if the database query fails.
|
||||
pub fn incident_exist(conn: &Connection, user_id: &str, book_id: &str, incident_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT 1 FROM book_incidents WHERE book_id=?1 AND incident_id=?2 AND author_id=?3")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'incident.".to_string() } else { "Unable to check incident existence.".to_string() }))?;
|
||||
|
||||
let existing_incident = statement
|
||||
.query_row(params![book_id, incident_id, user_id], |query_row| query_row.get::<_, i64>(0))
|
||||
.optional()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'incident.".to_string() } else { "Unable to check incident existence.".to_string() }))?;
|
||||
|
||||
Ok(existing_incident.is_some())
|
||||
}
|
||||
123
src-tauri/src/domains/incident/service.rs
Normal file
123
src-tauri/src/domains/incident/service.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use rusqlite::Connection;
|
||||
|
||||
use crate::crypto::encryption::{encrypt_data_with_user_key, decrypt_data_with_user_key, hash_element};
|
||||
use crate::crypto::key_manager::get_user_encryption_key;
|
||||
use crate::domains::chapter::repo::ActChapterQuery;
|
||||
use crate::domains::incident::repo;
|
||||
use crate::domains::tombstone::repo as tombstone_repo;
|
||||
use crate::error::AppResult;
|
||||
use crate::helpers::{create_unique_id, timestamp_in_seconds};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
/// Represents the story details of an incident within a chapter.
|
||||
pub struct IncidentStory {
|
||||
pub incident_title: String,
|
||||
pub incident_summary: String,
|
||||
pub chapter_summary: String,
|
||||
pub chapter_goal: String,
|
||||
}
|
||||
|
||||
/// Represents a synced incident with minimal information for comparison.
|
||||
pub struct SyncedIncident {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
/// Represents the properties of an incident with its associated chapters.
|
||||
pub struct IncidentProps {
|
||||
pub incident_id: String,
|
||||
pub title: String,
|
||||
pub summary: String,
|
||||
pub chapters: Vec<ActChapterQuery>,
|
||||
}
|
||||
|
||||
/// Creates a new incident for a book.
|
||||
/// Encrypts the incident name and generates a hashed version for indexing.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user creating the incident
|
||||
/// * `book_id` - The unique identifier of the book to add the incident to
|
||||
/// * `name` - The plain text name of the incident
|
||||
/// * `lang` - The language for error messages
|
||||
/// * `existing_incident_id` - Optional existing incident ID to use instead of generating a new one
|
||||
/// Returns the unique identifier of the created incident.
|
||||
pub fn add_new_incident(
|
||||
conn: &Connection, user_id: &str, book_id: &str, name: &str,
|
||||
lang: Lang, existing_incident_id: Option<&str>,
|
||||
) -> AppResult<String> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?;
|
||||
let hashed_name: String = hash_element(name);
|
||||
let incident_id: String = create_unique_id(existing_incident_id);
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
repo::insert_new_incident(conn, &incident_id, user_id, book_id, &encrypted_name, &hashed_name, last_update, lang)
|
||||
}
|
||||
|
||||
/// Retrieves all incidents for a specific book with their associated chapters.
|
||||
/// Decrypts incident titles and summaries using the user's encryption key.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `act_chapters` - Array of chapters from acts to associate with incidents
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns an array of incident properties with decrypted data.
|
||||
pub fn get_incitents_incidents(
|
||||
conn: &Connection, user_id: &str, book_id: &str,
|
||||
act_chapters: &[ActChapterQuery], lang: Lang,
|
||||
) -> AppResult<Vec<IncidentProps>> {
|
||||
let incident_query_results: Vec<repo::IncidentQuery> = repo::fetch_all_incitent_incidents(conn, user_id, book_id, lang)?;
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let mut incidents: Vec<IncidentProps> = Vec::new();
|
||||
|
||||
if !incident_query_results.is_empty() {
|
||||
for incident_record in &incident_query_results {
|
||||
let mut associated_chapters: Vec<ActChapterQuery> = Vec::new();
|
||||
for chapter in act_chapters {
|
||||
if let Some(ref incident_id) = chapter.incident_id {
|
||||
if incident_id == &incident_record.incident_id {
|
||||
associated_chapters.push(ActChapterQuery {
|
||||
chapter_info_id: chapter.chapter_info_id,
|
||||
chapter_id: chapter.chapter_id.clone(),
|
||||
title: chapter.title.clone(),
|
||||
chapter_order: chapter.chapter_order,
|
||||
act_id: chapter.act_id,
|
||||
incident_id: chapter.incident_id.clone(),
|
||||
plot_point_id: chapter.plot_point_id.clone(),
|
||||
summary: chapter.summary.clone(),
|
||||
goal: chapter.goal.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let decrypted_title: String = if incident_record.title.is_empty() { String::new() } else { decrypt_data_with_user_key(&incident_record.title, &user_key)? };
|
||||
let decrypted_summary: String = if incident_record.summary.is_empty() { String::new() } else { decrypt_data_with_user_key(&incident_record.summary, &user_key)? };
|
||||
incidents.push(IncidentProps {
|
||||
incident_id: incident_record.incident_id.clone(),
|
||||
title: decrypted_title,
|
||||
summary: decrypted_summary,
|
||||
chapters: associated_chapters,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(incidents)
|
||||
}
|
||||
|
||||
/// Removes an incident from a book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `incident_id` - The unique identifier of the incident to remove
|
||||
/// * `deleted_at` - The timestamp of deletion
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the incident was successfully deleted, false otherwise.
|
||||
pub fn remove_incident(
|
||||
conn: &Connection, user_id: &str, book_id: &str, incident_id: &str,
|
||||
deleted_at: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let deleted: bool = repo::delete_incident(conn, user_id, book_id, incident_id, lang)?;
|
||||
if deleted {
|
||||
tombstone_repo::insert(conn, incident_id, "book_incidents", incident_id, Some(book_id), user_id, deleted_at, lang)?;
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
2
src-tauri/src/domains/issue/mod.rs
Normal file
2
src-tauri/src/domains/issue/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod repo;
|
||||
pub mod service;
|
||||
225
src-tauri/src/domains/issue/repo.rs
Normal file
225
src-tauri/src/domains/issue/repo.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
pub struct BookIssuesTable {
|
||||
pub issue_id: String,
|
||||
pub author_id: String,
|
||||
pub book_id: String,
|
||||
pub name: String,
|
||||
pub hashed_issue_name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedIssueResult {
|
||||
pub issue_id: String,
|
||||
pub book_id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct IssueQuery {
|
||||
pub issue_id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Fetches all issues associated with a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user/author
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of issues with their IDs and names.
|
||||
pub fn fetch_issues_from_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<IssueQuery>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT issue_id, name FROM book_issues WHERE author_id=?1 AND book_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les problématiques.".to_string() } else { "Unable to retrieve issues.".to_string() }))?;
|
||||
|
||||
let issues = statement
|
||||
.query_map(params![user_id, book_id], |query_row| {
|
||||
Ok(IssueQuery { issue_id: query_row.get(0)?, name: query_row.get(1)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les problématiques.".to_string() } else { "Unable to retrieve issues.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les problématiques.".to_string() } else { "Unable to retrieve issues.".to_string() }))?;
|
||||
|
||||
Ok(issues)
|
||||
}
|
||||
|
||||
/// Inserts a new issue into the database after verifying it doesn't already exist.
|
||||
/// * `conn` - Database connection
|
||||
/// * `issue_id` - The unique identifier for the new issue
|
||||
/// * `user_id` - The unique identifier of the user/author
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `encrypted_name` - The encrypted name of the issue
|
||||
/// * `hashed_name` - The hashed name of the issue for duplicate checking
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the issue ID if successfully inserted.
|
||||
pub fn insert_new_issue(
|
||||
conn: &Connection, issue_id: &str, user_id: &str, book_id: &str,
|
||||
encrypted_name: &str, hashed_name: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<String> {
|
||||
let existing_issue: Option<String> = conn
|
||||
.query_row("SELECT issue_id FROM book_issues WHERE hashed_issue_name=?1 AND book_id=?2 AND author_id=?3", params![hashed_name, book_id, user_id], |query_row| query_row.get(0))
|
||||
.optional()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de la problématique.".to_string() } else { "Unable to verify issue existence.".to_string() }))?;
|
||||
|
||||
if existing_issue.is_some() {
|
||||
return Err(AppError::Validation(if lang == Lang::Fr { "La problématique existe déjà.".to_string() } else { "This issue already exists.".to_string() }));
|
||||
}
|
||||
|
||||
let insert_result = conn
|
||||
.execute("INSERT INTO book_issues (issue_id, author_id, book_id, name, hashed_issue_name, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![issue_id, user_id, book_id, encrypted_name, hashed_name, last_update])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter la problématique.".to_string() } else { "Unable to add issue.".to_string() }))?;
|
||||
|
||||
if insert_result == 0 {
|
||||
return Err(AppError::Internal(if lang == Lang::Fr { "Erreur pendant l'ajout de la problématique.".to_string() } else { "Error adding issue.".to_string() }));
|
||||
}
|
||||
|
||||
Ok(issue_id.to_string())
|
||||
}
|
||||
|
||||
/// Deletes an issue from the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user/author
|
||||
/// * `issue_id` - The unique identifier of the issue to delete
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the issue was successfully deleted, false otherwise.
|
||||
pub fn delete_issue(conn: &Connection, user_id: &str, issue_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let delete_result = conn
|
||||
.execute("DELETE FROM book_issues WHERE author_id=?1 AND issue_id=?2", params![user_id, issue_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer la problématique.".to_string() } else { "Unable to delete issue.".to_string() }))?;
|
||||
|
||||
Ok(delete_result > 0)
|
||||
}
|
||||
|
||||
/// Fetches all complete issue records for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user/author
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of complete issue records.
|
||||
pub fn fetch_book_issues(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookIssuesTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT issue_id, author_id, book_id, name, hashed_issue_name, last_update FROM book_issues WHERE author_id=?1 AND book_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les problématiques.".to_string() } else { "Unable to retrieve issues.".to_string() }))?;
|
||||
|
||||
let issues = statement
|
||||
.query_map(params![user_id, book_id], |query_row| {
|
||||
Ok(BookIssuesTable {
|
||||
issue_id: query_row.get(0)?, author_id: query_row.get(1)?,
|
||||
book_id: query_row.get(2)?, name: query_row.get(3)?,
|
||||
hashed_issue_name: query_row.get(4)?, last_update: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les problématiques.".to_string() } else { "Unable to retrieve issues.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les problématiques.".to_string() } else { "Unable to retrieve issues.".to_string() }))?;
|
||||
|
||||
Ok(issues)
|
||||
}
|
||||
|
||||
/// Fetches all synced issues for a specific user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user/author
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of synced issue records.
|
||||
pub fn fetch_synced_issues(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedIssueResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT issue_id, book_id, name, last_update FROM book_issues WHERE author_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les problématiques synchronisées.".to_string() } else { "Unable to retrieve synced issues.".to_string() }))?;
|
||||
|
||||
let synced_issues = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedIssueResult { issue_id: query_row.get(0)?, book_id: query_row.get(1)?, name: query_row.get(2)?, last_update: query_row.get(3)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les problématiques synchronisées.".to_string() } else { "Unable to retrieve synced issues.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les problématiques synchronisées.".to_string() } else { "Unable to retrieve synced issues.".to_string() }))?;
|
||||
|
||||
Ok(synced_issues)
|
||||
}
|
||||
|
||||
/// Inserts a synced issue from remote into the local database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `issue_id` - The unique identifier of the issue
|
||||
/// * `author_id` - The unique identifier of the author
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `name` - The encrypted name of the issue
|
||||
/// * `hashed_issue_name` - The hashed name of the issue
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the issue was successfully inserted, false otherwise.
|
||||
pub fn insert_sync_issue(
|
||||
conn: &Connection, issue_id: &str, author_id: &str, book_id: &str,
|
||||
name: &str, hashed_issue_name: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute("INSERT INTO book_issues (issue_id, author_id, book_id, name, hashed_issue_name, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![issue_id, author_id, book_id, name, hashed_issue_name, last_update])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer la problématique.".to_string() } else { "Unable to insert issue.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Fetches a complete issue record by its ID.
|
||||
/// * `conn` - Database connection
|
||||
/// * `issue_id` - The unique identifier of the issue
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of complete issue records.
|
||||
pub fn fetch_complete_issue_by_id(conn: &Connection, issue_id: &str, lang: Lang) -> AppResult<Vec<BookIssuesTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT issue_id, author_id, book_id, name, hashed_issue_name, last_update FROM book_issues WHERE issue_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le problème complet.".to_string() } else { "Unable to retrieve complete issue.".to_string() }))?;
|
||||
|
||||
let issues = statement
|
||||
.query_map(params![issue_id], |query_row| {
|
||||
Ok(BookIssuesTable {
|
||||
issue_id: query_row.get(0)?, author_id: query_row.get(1)?,
|
||||
book_id: query_row.get(2)?, name: query_row.get(3)?,
|
||||
hashed_issue_name: query_row.get(4)?, last_update: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le problème complet.".to_string() } else { "Unable to retrieve complete issue.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le problème complet.".to_string() } else { "Unable to retrieve complete issue.".to_string() }))?;
|
||||
|
||||
Ok(issues)
|
||||
}
|
||||
|
||||
/// Updates an existing issue in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user/author
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `issue_id` - The unique identifier of the issue to update
|
||||
/// * `name` - The new encrypted name of the issue
|
||||
/// * `hashed_name` - The new hashed name of the issue
|
||||
/// * `last_update` - The timestamp of the update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the issue was successfully updated, false otherwise.
|
||||
pub fn update_issue(
|
||||
conn: &Connection, user_id: &str, book_id: &str, issue_id: &str,
|
||||
name: &str, hashed_name: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute("UPDATE book_issues SET name = ?1, hashed_issue_name = ?2, last_update = ?3 WHERE issue_id = ?4 AND author_id = ?5 AND book_id = ?6", params![name, hashed_name, last_update, issue_id, user_id, book_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour la problématique.".to_string() } else { "Unable to update issue.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Checks if an issue exists in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user/author
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `issue_id` - The unique identifier of the issue to check
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the issue exists, false otherwise.
|
||||
pub fn issue_exist(conn: &Connection, user_id: &str, book_id: &str, issue_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let existing_issue: Option<i32> = conn
|
||||
.query_row("SELECT 1 FROM book_issues WHERE issue_id=?1 AND author_id=?2 AND book_id=?3", params![issue_id, user_id, book_id], |query_row| query_row.get(0))
|
||||
.optional()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du problème.".to_string() } else { "Unable to check issue existence.".to_string() }))?;
|
||||
|
||||
Ok(existing_issue.is_some())
|
||||
}
|
||||
82
src-tauri/src/domains/issue/service.rs
Normal file
82
src-tauri/src/domains/issue/service.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use rusqlite::Connection;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::crypto::encryption::{decrypt_data_with_user_key, encrypt_data_with_user_key, hash_element};
|
||||
use crate::crypto::key_manager::get_user_encryption_key;
|
||||
use crate::domains::issue::repo;
|
||||
use crate::domains::tombstone::repo as tombstone_repo;
|
||||
use crate::error::AppResult;
|
||||
use crate::helpers::{create_unique_id, timestamp_in_seconds};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
/// Represents a synced issue with its metadata.
|
||||
pub struct SyncedIssue {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct IssueProps {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Retrieves all issues associated with a specific book.
|
||||
/// Decrypts issue names using the user's encryption key.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns a list of decrypted issues.
|
||||
pub fn get_issues_from_book(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<IssueProps>> {
|
||||
let issue_query_results: Vec<repo::IssueQuery> = repo::fetch_issues_from_book(conn, user_id, book_id, lang)?;
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
let mut decrypted_issues: Vec<IssueProps> = Vec::new();
|
||||
|
||||
if !issue_query_results.is_empty() {
|
||||
for issue_record in issue_query_results {
|
||||
decrypted_issues.push(IssueProps {
|
||||
id: issue_record.issue_id,
|
||||
name: decrypt_data_with_user_key(&issue_record.name, &user_encryption_key)?,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(decrypted_issues)
|
||||
}
|
||||
|
||||
/// Creates a new issue for a book.
|
||||
/// Encrypts and hashes the issue name before storage.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `name` - The plain text name of the issue
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// * `existing_issue_id` - Optional existing issue ID for syncing purposes
|
||||
/// Returns the unique identifier of the created issue.
|
||||
pub fn add_new_issue(conn: &Connection, user_id: &str, book_id: &str, name: &str, lang: Lang, existing_issue_id: Option<&str>) -> AppResult<String> {
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
let encrypted_name: String = encrypt_data_with_user_key(name, &user_encryption_key)?;
|
||||
let hashed_name: String = hash_element(name);
|
||||
let issue_id: String = create_unique_id(existing_issue_id);
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
|
||||
repo::insert_new_issue(conn, &issue_id, user_id, book_id, &encrypted_name, &hashed_name, last_update, lang)
|
||||
}
|
||||
|
||||
/// Removes an issue from the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `issue_id` - The unique identifier of the issue to remove
|
||||
/// * `deleted_at` - The timestamp of deletion
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the issue was successfully removed, false otherwise.
|
||||
pub fn remove_issue(conn: &Connection, user_id: &str, book_id: &str, issue_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
|
||||
let deleted: bool = repo::delete_issue(conn, user_id, issue_id, lang)?;
|
||||
if deleted {
|
||||
tombstone_repo::insert(conn, book_id, "book_issues", issue_id, Some(book_id), user_id, deleted_at, lang)?;
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
158
src-tauri/src/domains/location/commands.rs
Normal file
158
src-tauri/src/domains/location/commands.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use serde::Deserialize;
|
||||
use tauri::State;
|
||||
|
||||
use crate::db::connection::DbManager;
|
||||
use crate::domains::location::service;
|
||||
use crate::error::AppError;
|
||||
use crate::shared::session::SessionState;
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
fn get_session(session: &State<SessionState>) -> Result<(String, Lang), AppError> {
|
||||
let session_guard = session.lock().map_err(|e| AppError::Internal(format!("Session lock failed: {}", e)))?;
|
||||
let user_id = session_guard.get_user_id().map_err(|e| AppError::Auth(e))?.to_string();
|
||||
let lang = session_guard.lang;
|
||||
Ok((user_id, lang))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetAllLocationsData {
|
||||
pub book_id: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_all_locations(data: GetAllLocationsData, db: State<DbManager>, session: State<SessionState>) -> Result<service::LocationListResponse, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_all_locations(conn, &user_id, &data.book_id, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddLocationSectionData {
|
||||
pub location_name: String,
|
||||
pub book_id: String,
|
||||
pub id: Option<String>,
|
||||
pub series_location_id: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_location_section(data: AddLocationSectionData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::add_location_section(conn, &user_id, &data.location_name, &data.book_id, lang, data.id.as_deref(), data.series_location_id.as_deref())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddLocationElementData {
|
||||
pub location_id: String,
|
||||
pub element_name: String,
|
||||
pub id: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_location_element(data: AddLocationElementData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::add_location_element(conn, &user_id, &data.location_id, &data.element_name, lang, data.id.as_deref())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddLocationSubElementData {
|
||||
pub element_id: String,
|
||||
pub sub_element_name: String,
|
||||
pub id: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_location_sub_element(data: AddLocationSubElementData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::add_location_sub_element(conn, &user_id, &data.element_id, &data.sub_element_name, lang, data.id.as_deref())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateLocationsData {
|
||||
pub locations: Vec<service::LocationProps>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_locations(data: UpdateLocationsData, db: State<DbManager>, session: State<SessionState>) -> Result<serde_json::Value, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
let (success, message) = service::update_location_section(conn, &user_id, &data.locations, lang)?;
|
||||
Ok(serde_json::json!({"success": success, "message": message}))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateLocationSectionWithSeriesLinkData {
|
||||
pub section_id: String,
|
||||
pub section_name: Option<String>,
|
||||
pub series_location_id: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_location_section_with_series_link(data: UpdateLocationSectionWithSeriesLinkData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::update_section_with_series_link(conn, &user_id, &data.section_id, data.section_name.as_deref(), data.series_location_id.as_deref(), lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteLocationSectionData {
|
||||
pub location_id: String,
|
||||
pub book_id: String,
|
||||
pub deleted_at: i64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_location_section(data: DeleteLocationSectionData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::delete_location_section(conn, &user_id, &data.book_id, &data.location_id, data.deleted_at, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteLocationElementData {
|
||||
pub element_id: String,
|
||||
pub book_id: String,
|
||||
pub deleted_at: i64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_location_element(data: DeleteLocationElementData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::delete_location_element(conn, &user_id, &data.book_id, &data.element_id, data.deleted_at, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteLocationSubElementData {
|
||||
pub sub_element_id: String,
|
||||
pub book_id: String,
|
||||
pub deleted_at: i64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_location_sub_element(data: DeleteLocationSubElementData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::delete_location_sub_element(conn, &user_id, &data.book_id, &data.sub_element_id, data.deleted_at, lang)
|
||||
}
|
||||
3
src-tauri/src/domains/location/mod.rs
Normal file
3
src-tauri/src/domains/location/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod commands;
|
||||
pub mod repo;
|
||||
pub mod service;
|
||||
788
src-tauri/src/domains/location/repo.rs
Normal file
788
src-tauri/src/domains/location/repo.rs
Normal file
@@ -0,0 +1,788 @@
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
pub struct LocationQueryResult {
|
||||
pub loc_id: String,
|
||||
pub loc_name: String,
|
||||
pub element_id: Option<String>,
|
||||
pub element_name: Option<String>,
|
||||
pub element_description: Option<String>,
|
||||
pub sub_element_id: Option<String>,
|
||||
pub sub_elem_name: Option<String>,
|
||||
pub sub_elem_description: Option<String>,
|
||||
pub series_location_id: Option<String>,
|
||||
}
|
||||
|
||||
pub struct LocationElementQueryResult {
|
||||
pub sub_element_id: Option<String>,
|
||||
pub sub_elem_name: Option<String>,
|
||||
pub sub_elem_description: Option<String>,
|
||||
pub element_id: String,
|
||||
pub element_name: String,
|
||||
pub element_description: Option<String>,
|
||||
}
|
||||
|
||||
pub struct LocationByTagResult {
|
||||
pub element_name: String,
|
||||
pub element_description: Option<String>,
|
||||
pub sub_elem_name: Option<String>,
|
||||
pub sub_elem_description: Option<String>,
|
||||
}
|
||||
|
||||
pub struct BookLocationTable {
|
||||
pub loc_id: String,
|
||||
pub book_id: String,
|
||||
pub user_id: String,
|
||||
pub loc_name: String,
|
||||
pub loc_original_name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct LocationElementTable {
|
||||
pub element_id: String,
|
||||
pub location: String,
|
||||
pub user_id: String,
|
||||
pub element_name: String,
|
||||
pub original_name: String,
|
||||
pub element_description: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct LocationSubElementTable {
|
||||
pub sub_element_id: String,
|
||||
pub element_id: String,
|
||||
pub user_id: String,
|
||||
pub sub_elem_name: String,
|
||||
pub original_name: String,
|
||||
pub sub_elem_description: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedLocationResult {
|
||||
pub loc_id: String,
|
||||
pub book_id: String,
|
||||
pub loc_name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedLocationElementResult {
|
||||
pub element_id: String,
|
||||
pub location: String,
|
||||
pub element_name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedLocationSubElementResult {
|
||||
pub sub_element_id: String,
|
||||
pub element_id: String,
|
||||
pub sub_elem_name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
/// Retrieves all locations with their elements and sub-elements for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `book_id` - The book's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of location query results with nested elements.
|
||||
pub fn get_location(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<LocationQueryResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT loc_id, loc_name, element.element_id AS element_id, element.element_name, element.element_description, sub_elem.sub_element_id AS sub_element_id, sub_elem.sub_elem_name, sub_elem.sub_elem_description, location.series_location_id FROM book_location AS location LEFT JOIN location_element AS element ON location.loc_id = element.location LEFT JOIN location_sub_element AS sub_elem ON element.element_id = sub_elem.element_id WHERE location.user_id = ?1 AND location.book_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les emplacements.".to_string() } else { "Unable to retrieve locations.".to_string() }))?;
|
||||
|
||||
let locations = statement
|
||||
.query_map(params![user_id, book_id], |query_row| {
|
||||
Ok(LocationQueryResult {
|
||||
loc_id: query_row.get(0)?,
|
||||
loc_name: query_row.get(1)?,
|
||||
element_id: query_row.get(2)?,
|
||||
element_name: query_row.get(3)?,
|
||||
element_description: query_row.get(4)?,
|
||||
sub_element_id: query_row.get(5)?,
|
||||
sub_elem_name: query_row.get(6)?,
|
||||
sub_elem_description: query_row.get(7)?,
|
||||
series_location_id: query_row.get(8)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les emplacements.".to_string() } else { "Unable to retrieve locations.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les emplacements.".to_string() } else { "Unable to retrieve locations.".to_string() }))?;
|
||||
|
||||
Ok(locations)
|
||||
}
|
||||
|
||||
/// Inserts a new location section for a book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `location_id` - The new location's unique identifier
|
||||
/// * `book_id` - The book's unique identifier
|
||||
/// * `encrypted_name` - The encrypted location name
|
||||
/// * `original_name` - The original (unencrypted) location name
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `series_location_id` - The series location ID (optional)
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the location ID if insertion was successful.
|
||||
pub fn insert_location(
|
||||
conn: &Connection, user_id: &str, location_id: &str, book_id: &str, encrypted_name: &str,
|
||||
original_name: &str, last_update: i64, series_location_id: Option<&str>, lang: Lang,
|
||||
) -> AppResult<String> {
|
||||
let insert_result = match series_location_id {
|
||||
Some(series_id) => conn.execute(
|
||||
"INSERT INTO book_location (loc_id, book_id, user_id, loc_name, loc_original_name, series_location_id, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![location_id, book_id, user_id, encrypted_name, original_name, series_id, last_update],
|
||||
),
|
||||
None => conn.execute(
|
||||
"INSERT INTO book_location (loc_id, book_id, user_id, loc_name, loc_original_name, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![location_id, book_id, user_id, encrypted_name, original_name, last_update],
|
||||
),
|
||||
}
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter la section d'emplacement.".to_string() } else { "Unable to add location section.".to_string() }))?;
|
||||
|
||||
if insert_result > 0 {
|
||||
Ok(location_id.to_string())
|
||||
} else {
|
||||
Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de l'ajout de la section d'emplacement.".to_string() } else { "Error adding location section.".to_string() }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a new location element within a location section.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `element_id` - The new element's unique identifier
|
||||
/// * `location_id` - The parent location's unique identifier
|
||||
/// * `encrypted_name` - The encrypted element name
|
||||
/// * `original_name` - The original (unencrypted) element name
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the element ID if insertion was successful.
|
||||
pub fn insert_location_element(
|
||||
conn: &Connection, user_id: &str, element_id: &str, location_id: &str,
|
||||
encrypted_name: &str, original_name: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<String> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO location_element (element_id, location, user_id, element_name, original_name, element_description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![element_id, location_id, user_id, encrypted_name, original_name, "", last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter l'élément d'emplacement.".to_string() } else { "Unable to add location element.".to_string() }))?;
|
||||
|
||||
if insert_result > 0 {
|
||||
Ok(element_id.to_string())
|
||||
} else {
|
||||
Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de l'ajout de l'élément d'emplacement.".to_string() } else { "Error adding location element.".to_string() }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a new sub-element within a location element.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `sub_element_id` - The new sub-element's unique identifier
|
||||
/// * `element_id` - The parent element's unique identifier
|
||||
/// * `encrypted_name` - The encrypted sub-element name
|
||||
/// * `original_name` - The original (unencrypted) sub-element name
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the sub-element ID if insertion was successful.
|
||||
pub fn insert_location_sub_element(
|
||||
conn: &Connection, user_id: &str, sub_element_id: &str, element_id: &str,
|
||||
encrypted_name: &str, original_name: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<String> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO location_sub_element (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![sub_element_id, element_id, user_id, encrypted_name, original_name, "", last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le sous-élément d'emplacement.".to_string() } else { "Unable to add location sub-element.".to_string() }))?;
|
||||
|
||||
if insert_result > 0 {
|
||||
Ok(sub_element_id.to_string())
|
||||
} else {
|
||||
Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de l'ajout du sous-élément d'emplacement.".to_string() } else { "Error adding location sub-element.".to_string() }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates an existing location sub-element's name and description.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `id` - The sub-element's unique identifier
|
||||
/// * `encrypted_name` - The new encrypted sub-element name
|
||||
/// * `original_name` - The new original (unencrypted) sub-element name
|
||||
/// * `encrypt_description` - The new encrypted description
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the update affected at least one row.
|
||||
pub fn update_location_sub_element(
|
||||
conn: &Connection, user_id: &str, id: &str, encrypted_name: &str,
|
||||
original_name: &str, encrypt_description: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute(
|
||||
"UPDATE location_sub_element SET sub_elem_name = ?1, original_name = ?2, sub_elem_description = ?3, last_update = ?4 WHERE sub_element_id = ?5 AND user_id = ?6",
|
||||
params![encrypted_name, original_name, encrypt_description, last_update, id, user_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le sous-élément d'emplacement.".to_string() } else { "Unable to update location sub-element.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Updates an existing location element's name and description.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `id` - The element's unique identifier
|
||||
/// * `encrypted_name` - The new encrypted element name
|
||||
/// * `original_name` - The new original (unencrypted) element name
|
||||
/// * `encrypted_description` - The new encrypted description
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the update affected at least one row.
|
||||
pub fn update_location_element(
|
||||
conn: &Connection, user_id: &str, id: &str, encrypted_name: &str,
|
||||
original_name: &str, encrypted_description: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute(
|
||||
"UPDATE location_element SET element_name = ?1, original_name = ?2, element_description = ?3, last_update = ?4 WHERE element_id = ?5 AND user_id = ?6",
|
||||
params![encrypted_name, original_name, encrypted_description, last_update, id, user_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour l'élément d'emplacement.".to_string() } else { "Unable to update location element.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Updates an existing location section's name.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `id` - The location section's unique identifier
|
||||
/// * `encrypted_name` - The new encrypted location name
|
||||
/// * `original_name` - The new original (unencrypted) location name
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the update affected at least one row.
|
||||
pub fn update_location_section(
|
||||
conn: &Connection, user_id: &str, id: &str, encrypted_name: &str,
|
||||
original_name: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute(
|
||||
"UPDATE book_location SET loc_name = ?1, loc_original_name = ?2, last_update = ?3 WHERE loc_id = ?4 AND user_id = ?5",
|
||||
params![encrypted_name, original_name, last_update, id, user_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour la section d'emplacement.".to_string() } else { "Unable to update location section.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Deletes a location section by its ID.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `location_id` - The location section's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the deletion affected at least one row.
|
||||
pub fn delete_location_section(conn: &Connection, user_id: &str, location_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let delete_result = conn
|
||||
.execute("DELETE FROM book_location WHERE loc_id = ?1 AND user_id = ?2", params![location_id, user_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer la section d'emplacement.".to_string() } else { "Unable to delete location section.".to_string() }))?;
|
||||
|
||||
Ok(delete_result > 0)
|
||||
}
|
||||
|
||||
/// Deletes a location element by its ID.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `element_id` - The element's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the deletion affected at least one row.
|
||||
pub fn delete_location_element(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let delete_result = conn
|
||||
.execute("DELETE FROM location_element WHERE element_id = ?1 AND user_id = ?2", params![element_id, user_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer l'élément d'emplacement.".to_string() } else { "Unable to delete location element.".to_string() }))?;
|
||||
|
||||
Ok(delete_result > 0)
|
||||
}
|
||||
|
||||
/// Deletes a location sub-element by its ID.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `sub_element_id` - The sub-element's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the deletion affected at least one row.
|
||||
pub fn delete_location_sub_element(conn: &Connection, user_id: &str, sub_element_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let delete_result = conn
|
||||
.execute("DELETE FROM location_sub_element WHERE sub_element_id = ?1 AND user_id = ?2", params![sub_element_id, user_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le sous-élément d'emplacement.".to_string() } else { "Unable to delete location sub-element.".to_string() }))?;
|
||||
|
||||
Ok(delete_result > 0)
|
||||
}
|
||||
|
||||
/// Fetches all location elements and sub-elements for tagging purposes.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `book_id` - The book's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of location elements with their sub-elements.
|
||||
pub fn fetch_location_tags(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<LocationElementQueryResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT se.sub_element_id AS sub_element_id, se.sub_elem_name, se.sub_elem_description, el.element_id AS element_id, el.element_name, el.element_description FROM location_sub_element AS se RIGHT JOIN location_element AS el ON se.element_id = el.element_id LEFT JOIN book_location AS lo ON el.location = lo.loc_id WHERE lo.book_id = ?1 AND lo.user_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags d'emplacement.".to_string() } else { "Unable to retrieve location tags.".to_string() }))?;
|
||||
|
||||
let location_tags = statement
|
||||
.query_map(params![book_id, user_id], |query_row| {
|
||||
Ok(LocationElementQueryResult {
|
||||
sub_element_id: query_row.get(0)?,
|
||||
sub_elem_name: query_row.get(1)?,
|
||||
sub_elem_description: query_row.get(2)?,
|
||||
element_id: query_row.get(3)?,
|
||||
element_name: query_row.get(4)?,
|
||||
element_description: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags d'emplacement.".to_string() } else { "Unable to retrieve location tags.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les tags d'emplacement.".to_string() } else { "Unable to retrieve location tags.".to_string() }))?;
|
||||
|
||||
Ok(location_tags)
|
||||
}
|
||||
|
||||
/// Fetches locations by their tag IDs (element or sub-element IDs).
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `locations` - An array of location tag IDs to search for
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of locations matching the provided tags.
|
||||
/// Errors if no tags are provided or no locations are found.
|
||||
pub fn fetch_locations_by_tags(conn: &Connection, user_id: &str, locations: &[String], lang: Lang) -> AppResult<Vec<LocationByTagResult>> {
|
||||
if locations.is_empty() {
|
||||
return Err(AppError::Validation(if lang == Lang::Fr { "Aucun tag fourni.".to_string() } else { "No tags provided.".to_string() }));
|
||||
}
|
||||
|
||||
let location_placeholders: String = locations.iter().map(|_| "?").collect::<Vec<_>>().join(",");
|
||||
let query = format!("SELECT el.element_name, el.element_description, se.sub_elem_name, se.sub_elem_description FROM location_element AS el LEFT JOIN location_sub_element AS se ON el.element_id = se.element_id WHERE el.user_id = ?1 AND (el.element_id IN ({}) OR se.sub_element_id IN ({}))", location_placeholders, location_placeholders);
|
||||
|
||||
let mut statement = conn
|
||||
.prepare(&query)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les emplacements par tags.".to_string() } else { "Unable to retrieve locations by tags.".to_string() }))?;
|
||||
|
||||
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
|
||||
param_values.push(Box::new(user_id.to_string()));
|
||||
for location in locations {
|
||||
param_values.push(Box::new(location.clone()));
|
||||
}
|
||||
for location in locations {
|
||||
param_values.push(Box::new(location.clone()));
|
||||
}
|
||||
let param_refs: Vec<&dyn rusqlite::types::ToSql> = param_values.iter().map(|parameter| parameter.as_ref() as &dyn rusqlite::types::ToSql).collect();
|
||||
|
||||
let locations_by_tags = statement
|
||||
.query_map(param_refs.as_slice(), |query_row| {
|
||||
Ok(LocationByTagResult {
|
||||
element_name: query_row.get(0)?,
|
||||
element_description: query_row.get(1)?,
|
||||
sub_elem_name: query_row.get(2)?,
|
||||
sub_elem_description: query_row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les emplacements par tags.".to_string() } else { "Unable to retrieve locations by tags.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les emplacements par tags.".to_string() } else { "Unable to retrieve locations by tags.".to_string() }))?;
|
||||
|
||||
if locations_by_tags.is_empty() {
|
||||
return Err(AppError::NotFound(if lang == Lang::Fr { "Aucun emplacement trouvé avec ces tags.".to_string() } else { "No locations found with these tags.".to_string() }));
|
||||
}
|
||||
|
||||
Ok(locations_by_tags)
|
||||
}
|
||||
|
||||
/// Checks if a location exists in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `loc_id` - The location's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the location exists, false otherwise.
|
||||
pub fn is_location_exist(conn: &Connection, user_id: &str, loc_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT 1 FROM book_location WHERE loc_id = ?1 AND user_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'emplacement.".to_string() } else { "Unable to check location existence.".to_string() }))?;
|
||||
|
||||
let existing_location = statement
|
||||
.query_row(params![loc_id, user_id], |query_row| query_row.get::<_, i32>(0))
|
||||
.optional()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'emplacement.".to_string() } else { "Unable to check location existence.".to_string() }))?;
|
||||
|
||||
Ok(existing_location.is_some())
|
||||
}
|
||||
|
||||
/// Checks if a location element exists in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `element_id` - The element's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the location element exists, false otherwise.
|
||||
pub fn is_location_element_exist(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT 1 FROM location_element WHERE element_id = ?1 AND user_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'élément d'emplacement.".to_string() } else { "Unable to check location element existence.".to_string() }))?;
|
||||
|
||||
let existing_element = statement
|
||||
.query_row(params![element_id, user_id], |query_row| query_row.get::<_, i32>(0))
|
||||
.optional()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de l'élément d'emplacement.".to_string() } else { "Unable to check location element existence.".to_string() }))?;
|
||||
|
||||
Ok(existing_element.is_some())
|
||||
}
|
||||
|
||||
/// Checks if a location sub-element exists in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `sub_element_id` - The sub-element's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the location sub-element exists, false otherwise.
|
||||
pub fn is_location_sub_element_exist(conn: &Connection, user_id: &str, sub_element_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT 1 FROM location_sub_element WHERE sub_element_id = ?1 AND user_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du sous-élément d'emplacement.".to_string() } else { "Unable to check location sub-element existence.".to_string() }))?;
|
||||
|
||||
let existing_sub_element = statement
|
||||
.query_row(params![sub_element_id, user_id], |query_row| query_row.get::<_, i32>(0))
|
||||
.optional()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du sous-élément d'emplacement.".to_string() } else { "Unable to check location sub-element existence.".to_string() }))?;
|
||||
|
||||
Ok(existing_sub_element.is_some())
|
||||
}
|
||||
|
||||
/// Fetches all locations for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `book_id` - The book's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of book location records.
|
||||
pub fn fetch_book_locations(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookLocationTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT loc_id, book_id, user_id, loc_name, loc_original_name, last_update FROM book_location WHERE user_id = ?1 AND book_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux.".to_string() } else { "Unable to retrieve locations.".to_string() }))?;
|
||||
|
||||
let book_locations = statement
|
||||
.query_map(params![user_id, book_id], |query_row| {
|
||||
Ok(BookLocationTable {
|
||||
loc_id: query_row.get(0)?, book_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, loc_name: query_row.get(3)?,
|
||||
loc_original_name: query_row.get(4)?, last_update: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux.".to_string() } else { "Unable to retrieve locations.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux.".to_string() } else { "Unable to retrieve locations.".to_string() }))?;
|
||||
|
||||
Ok(book_locations)
|
||||
}
|
||||
|
||||
/// Fetches all elements for a specific location.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `location_id` - The location's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of location element records.
|
||||
pub fn fetch_location_elements(conn: &Connection, user_id: &str, location_id: &str, lang: Lang) -> AppResult<Vec<LocationElementTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT element_id, location, user_id, element_name, original_name, element_description, last_update FROM location_element WHERE user_id = ?1 AND location = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu.".to_string() } else { "Unable to retrieve location elements.".to_string() }))?;
|
||||
|
||||
let location_elements = statement
|
||||
.query_map(params![user_id, location_id], |query_row| {
|
||||
Ok(LocationElementTable {
|
||||
element_id: query_row.get(0)?, location: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, element_name: query_row.get(3)?,
|
||||
original_name: query_row.get(4)?, element_description: query_row.get(5)?,
|
||||
last_update: query_row.get(6)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu.".to_string() } else { "Unable to retrieve location elements.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu.".to_string() } else { "Unable to retrieve location elements.".to_string() }))?;
|
||||
|
||||
Ok(location_elements)
|
||||
}
|
||||
|
||||
/// Fetches all sub-elements for a specific location element.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `element_id` - The element's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of location sub-element records.
|
||||
pub fn fetch_location_sub_elements(conn: &Connection, user_id: &str, element_id: &str, lang: Lang) -> AppResult<Vec<LocationSubElementTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update FROM location_sub_element WHERE user_id = ?1 AND element_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu.".to_string() } else { "Unable to retrieve location sub-elements.".to_string() }))?;
|
||||
|
||||
let location_sub_elements = statement
|
||||
.query_map(params![user_id, element_id], |query_row| {
|
||||
Ok(LocationSubElementTable {
|
||||
sub_element_id: query_row.get(0)?, element_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, sub_elem_name: query_row.get(3)?,
|
||||
original_name: query_row.get(4)?, sub_elem_description: query_row.get(5)?,
|
||||
last_update: query_row.get(6)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu.".to_string() } else { "Unable to retrieve location sub-elements.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu.".to_string() } else { "Unable to retrieve location sub-elements.".to_string() }))?;
|
||||
|
||||
Ok(location_sub_elements)
|
||||
}
|
||||
|
||||
/// Fetches all synced locations for a user (used for synchronization).
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of synced location records.
|
||||
pub fn fetch_synced_locations(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedLocationResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT loc_id, book_id, loc_name, last_update FROM book_location WHERE user_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux synchronisés.".to_string() } else { "Unable to retrieve synced locations.".to_string() }))?;
|
||||
|
||||
let synced_locations = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedLocationResult { loc_id: query_row.get(0)?, book_id: query_row.get(1)?, loc_name: query_row.get(2)?, last_update: query_row.get(3)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux synchronisés.".to_string() } else { "Unable to retrieve synced locations.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les lieux synchronisés.".to_string() } else { "Unable to retrieve synced locations.".to_string() }))?;
|
||||
|
||||
Ok(synced_locations)
|
||||
}
|
||||
|
||||
/// Fetches all synced location elements for a user (used for synchronization).
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of synced location element records.
|
||||
pub fn fetch_synced_location_elements(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedLocationElementResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT element_id, location, element_name, last_update FROM location_element WHERE user_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu synchronisés.".to_string() } else { "Unable to retrieve synced location elements.".to_string() }))?;
|
||||
|
||||
let synced_location_elements = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedLocationElementResult { element_id: query_row.get(0)?, location: query_row.get(1)?, element_name: query_row.get(2)?, last_update: query_row.get(3)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu synchronisés.".to_string() } else { "Unable to retrieve synced location elements.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les éléments de lieu synchronisés.".to_string() } else { "Unable to retrieve synced location elements.".to_string() }))?;
|
||||
|
||||
Ok(synced_location_elements)
|
||||
}
|
||||
|
||||
/// Fetches all synced location sub-elements for a user (used for synchronization).
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of synced location sub-element records.
|
||||
pub fn fetch_synced_location_sub_elements(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedLocationSubElementResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT sub_element_id, element_id, sub_elem_name, last_update FROM location_sub_element WHERE user_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu synchronisés.".to_string() } else { "Unable to retrieve synced location sub-elements.".to_string() }))?;
|
||||
|
||||
let synced_location_sub_elements = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedLocationSubElementResult { sub_element_id: query_row.get(0)?, element_id: query_row.get(1)?, sub_elem_name: query_row.get(2)?, last_update: query_row.get(3)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu synchronisés.".to_string() } else { "Unable to retrieve synced location sub-elements.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les sous-éléments de lieu synchronisés.".to_string() } else { "Unable to retrieve synced location sub-elements.".to_string() }))?;
|
||||
|
||||
Ok(synced_location_sub_elements)
|
||||
}
|
||||
|
||||
/// Inserts a synced location from the remote server.
|
||||
/// * `conn` - Database connection
|
||||
/// * `loc_id` - The location's unique identifier
|
||||
/// * `book_id` - The book's unique identifier
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `loc_name` - The encrypted location name
|
||||
/// * `loc_original_name` - The original (unencrypted) location name
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the insertion affected at least one row.
|
||||
pub fn insert_sync_location(
|
||||
conn: &Connection, loc_id: &str, book_id: &str, user_id: &str,
|
||||
loc_name: &str, loc_original_name: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO book_location (loc_id, book_id, user_id, loc_name, loc_original_name, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![loc_id, book_id, user_id, loc_name, loc_original_name, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le lieu.".to_string() } else { "Unable to insert location.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Inserts a synced location element from the remote server.
|
||||
/// * `conn` - Database connection
|
||||
/// * `element_id` - The element's unique identifier
|
||||
/// * `location` - The parent location's unique identifier
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `element_name` - The encrypted element name
|
||||
/// * `original_name` - The original (unencrypted) element name
|
||||
/// * `element_description` - The encrypted element description (can be null)
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the insertion affected at least one row.
|
||||
pub fn insert_sync_location_element(
|
||||
conn: &Connection, element_id: &str, location: &str, user_id: &str, element_name: &str,
|
||||
original_name: &str, element_description: Option<&str>, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO location_element (element_id, location, user_id, element_name, original_name, element_description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![element_id, location, user_id, element_name, original_name, element_description, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer l'élément du lieu.".to_string() } else { "Unable to insert location element.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Inserts a synced location sub-element from the remote server.
|
||||
/// * `conn` - Database connection
|
||||
/// * `sub_element_id` - The sub-element's unique identifier
|
||||
/// * `element_id` - The parent element's unique identifier
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `sub_elem_name` - The encrypted sub-element name
|
||||
/// * `original_name` - The original (unencrypted) sub-element name
|
||||
/// * `sub_elem_description` - The encrypted sub-element description (can be null)
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the insertion affected at least one row.
|
||||
pub fn insert_sync_location_sub_element(
|
||||
conn: &Connection, sub_element_id: &str, element_id: &str, user_id: &str, sub_elem_name: &str,
|
||||
original_name: &str, sub_elem_description: Option<&str>, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO location_sub_element (sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le sous-élément du lieu.".to_string() } else { "Unable to insert location sub-element.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Fetches complete location data by its ID (without user filtering).
|
||||
/// * `conn` - Database connection
|
||||
/// * `id` - The location's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of book location records.
|
||||
pub fn fetch_complete_location_by_id(conn: &Connection, id: &str, lang: Lang) -> AppResult<Vec<BookLocationTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT loc_id, book_id, user_id, loc_name, loc_original_name, last_update FROM book_location WHERE loc_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le lieu complet.".to_string() } else { "Unable to retrieve complete location.".to_string() }))?;
|
||||
|
||||
let complete_location = statement
|
||||
.query_map(params![id], |query_row| {
|
||||
Ok(BookLocationTable {
|
||||
loc_id: query_row.get(0)?, book_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, loc_name: query_row.get(3)?,
|
||||
loc_original_name: query_row.get(4)?, last_update: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le lieu complet.".to_string() } else { "Unable to retrieve complete location.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le lieu complet.".to_string() } else { "Unable to retrieve complete location.".to_string() }))?;
|
||||
|
||||
Ok(complete_location)
|
||||
}
|
||||
|
||||
/// Fetches complete location element data by its ID (without user filtering).
|
||||
/// * `conn` - Database connection
|
||||
/// * `id` - The element's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of location element records.
|
||||
pub fn fetch_complete_location_element_by_id(conn: &Connection, id: &str, lang: Lang) -> AppResult<Vec<LocationElementTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT element_id, location, user_id, element_name, original_name, element_description, last_update FROM location_element WHERE element_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'élément de lieu complet.".to_string() } else { "Unable to retrieve complete location element.".to_string() }))?;
|
||||
|
||||
let complete_location_element = statement
|
||||
.query_map(params![id], |query_row| {
|
||||
Ok(LocationElementTable {
|
||||
element_id: query_row.get(0)?, location: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, element_name: query_row.get(3)?,
|
||||
original_name: query_row.get(4)?, element_description: query_row.get(5)?,
|
||||
last_update: query_row.get(6)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'élément de lieu complet.".to_string() } else { "Unable to retrieve complete location element.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer l'élément de lieu complet.".to_string() } else { "Unable to retrieve complete location element.".to_string() }))?;
|
||||
|
||||
Ok(complete_location_element)
|
||||
}
|
||||
|
||||
/// Fetches complete location sub-element data by its ID (without user filtering).
|
||||
/// * `conn` - Database connection
|
||||
/// * `id` - The sub-element's unique identifier
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of location sub-element records.
|
||||
pub fn fetch_complete_location_sub_element_by_id(conn: &Connection, id: &str, lang: Lang) -> AppResult<Vec<LocationSubElementTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT sub_element_id, element_id, user_id, sub_elem_name, original_name, sub_elem_description, last_update FROM location_sub_element WHERE sub_element_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sous-élément de lieu complet.".to_string() } else { "Unable to retrieve complete location sub-element.".to_string() }))?;
|
||||
|
||||
let complete_location_sub_element = statement
|
||||
.query_map(params![id], |query_row| {
|
||||
Ok(LocationSubElementTable {
|
||||
sub_element_id: query_row.get(0)?, element_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, sub_elem_name: query_row.get(3)?,
|
||||
original_name: query_row.get(4)?, sub_elem_description: query_row.get(5)?,
|
||||
last_update: query_row.get(6)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sous-élément de lieu complet.".to_string() } else { "Unable to retrieve complete location sub-element.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le sous-élément de lieu complet.".to_string() } else { "Unable to retrieve complete location sub-element.".to_string() }))?;
|
||||
|
||||
Ok(complete_location_sub_element)
|
||||
}
|
||||
|
||||
/// Updates a location section with optional name change and series link.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `section_id` - The section's unique identifier
|
||||
/// * `encrypted_name` - The new encrypted name (optional)
|
||||
/// * `original_name` - The new original name (optional)
|
||||
/// * `series_location_id` - The series location ID to link (optional, None to unlink)
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the update was successful.
|
||||
pub fn update_section_with_series_link(
|
||||
conn: &Connection, user_id: &str, section_id: &str, encrypted_name: Option<&str>,
|
||||
original_name: Option<&str>, series_location_id: Option<&str>, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let mut set_clauses: Vec<String> = vec![format!("last_update={}", last_update)];
|
||||
let mut param_values: Vec<Box<dyn rusqlite::types::ToSql>> = Vec::new();
|
||||
|
||||
if let (Some(enc_name), Some(orig_name)) = (encrypted_name, original_name) {
|
||||
set_clauses.push("loc_name=?".to_string());
|
||||
set_clauses.push("loc_original_name=?".to_string());
|
||||
param_values.push(Box::new(enc_name.to_string()));
|
||||
param_values.push(Box::new(orig_name.to_string()));
|
||||
}
|
||||
|
||||
if let Some(series_id) = series_location_id {
|
||||
set_clauses.push("series_location_id=?".to_string());
|
||||
param_values.push(Box::new(series_id.to_string()));
|
||||
}
|
||||
|
||||
param_values.push(Box::new(section_id.to_string()));
|
||||
param_values.push(Box::new(user_id.to_string()));
|
||||
|
||||
let query = format!("UPDATE book_location SET {} WHERE loc_id=? AND user_id=?", set_clauses.join(", "));
|
||||
let param_refs: Vec<&dyn rusqlite::types::ToSql> = param_values.iter().map(|parameter| parameter.as_ref() as &dyn rusqlite::types::ToSql).collect();
|
||||
|
||||
let update_result = conn
|
||||
.execute(&query, param_refs.as_slice())
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour la section d'emplacement.".to_string() } else { "Unable to update location section.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
462
src-tauri/src/domains/location/service.rs
Normal file
462
src-tauri/src/domains/location/service.rs
Normal file
@@ -0,0 +1,462 @@
|
||||
use rusqlite::Connection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::crypto::encryption::{encrypt_data_with_user_key, decrypt_data_with_user_key, hash_element};
|
||||
use crate::crypto::key_manager::get_user_encryption_key;
|
||||
use crate::domains::book::repo as book_repo;
|
||||
use crate::domains::location::repo;
|
||||
use crate::domains::tombstone::repo as tombstone_repo;
|
||||
use crate::error::AppResult;
|
||||
use crate::helpers::{create_unique_id, timestamp_in_seconds};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SubElement {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Element {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub sub_elements: Vec<SubElement>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LocationProps {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub elements: Vec<Element>,
|
||||
pub series_location_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct LocationListResponse {
|
||||
pub locations: Vec<LocationProps>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
/// Synced location sub-element (lightweight, for comparison).
|
||||
pub struct SyncedLocationSubElement {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
/// Synced location element (lightweight, for comparison).
|
||||
pub struct SyncedLocationElement {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
pub sub_elements: Vec<SyncedLocationSubElement>,
|
||||
}
|
||||
|
||||
/// Synced location (lightweight, for comparison).
|
||||
pub struct SyncedLocation {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
pub elements: Vec<SyncedLocationElement>,
|
||||
}
|
||||
|
||||
/// Retrieves all locations for a given user and book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `book_id` - The book's unique identifier
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns LocationListResponse containing an array of locations and enabled flag.
|
||||
pub fn get_all_locations(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<LocationListResponse> {
|
||||
let book_tools: Option<book_repo::BookToolsTable> = book_repo::fetch_book_tools(conn, user_id, book_id, lang)?;
|
||||
let enabled: bool = book_tools.map_or(false, |book_tools_row| book_tools_row.locations_enabled == 1);
|
||||
|
||||
let location_records: Vec<repo::LocationQueryResult> = repo::get_location(conn, user_id, book_id, lang)?;
|
||||
if location_records.is_empty() {
|
||||
return Ok(LocationListResponse { locations: vec![], enabled });
|
||||
}
|
||||
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let mut location_array: Vec<LocationProps> = Vec::new();
|
||||
|
||||
for record in &location_records {
|
||||
let location_index: Option<usize> = location_array.iter().position(|loc| loc.id == record.loc_id);
|
||||
|
||||
let location_idx: usize = match location_index {
|
||||
Some(idx) => idx,
|
||||
None => {
|
||||
let decrypted_name: String = decrypt_data_with_user_key(&record.loc_name, &user_key)?;
|
||||
location_array.push(LocationProps {
|
||||
id: record.loc_id.clone(),
|
||||
name: decrypted_name,
|
||||
elements: vec![],
|
||||
series_location_id: record.series_location_id.clone(),
|
||||
});
|
||||
location_array.len() - 1
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ref element_id) = record.element_id {
|
||||
let element_index: Option<usize> = location_array[location_idx].elements.iter().position(|elem| elem.id == *element_id);
|
||||
|
||||
let element_idx: usize = match element_index {
|
||||
Some(idx) => idx,
|
||||
None => {
|
||||
let decrypted_name: String = decrypt_data_with_user_key(record.element_name.as_deref().unwrap_or(""), &user_key)?;
|
||||
let decrypted_description: String = if let Some(ref element_description) = record.element_description {
|
||||
decrypt_data_with_user_key(element_description, &user_key)?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
location_array[location_idx].elements.push(Element {
|
||||
id: element_id.clone(),
|
||||
name: decrypted_name,
|
||||
description: decrypted_description,
|
||||
sub_elements: vec![],
|
||||
});
|
||||
location_array[location_idx].elements.len() - 1
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ref sub_element_id) = record.sub_element_id {
|
||||
let sub_element_exists: bool = location_array[location_idx].elements[element_idx]
|
||||
.sub_elements
|
||||
.iter()
|
||||
.any(|sub| sub.id == *sub_element_id);
|
||||
|
||||
if !sub_element_exists {
|
||||
let decrypted_name: String = decrypt_data_with_user_key(record.sub_elem_name.as_deref().unwrap_or(""), &user_key)?;
|
||||
let decrypted_description: String = if let Some(ref sub_elem_description) = record.sub_elem_description {
|
||||
decrypt_data_with_user_key(sub_elem_description, &user_key)?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
location_array[location_idx].elements[element_idx].sub_elements.push(SubElement {
|
||||
id: sub_element_id.clone(),
|
||||
name: decrypted_name,
|
||||
description: decrypted_description,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(LocationListResponse { locations: location_array, enabled })
|
||||
}
|
||||
|
||||
/// Adds a new location section for a book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `location_name` - The name of the location to create
|
||||
/// * `book_id` - The book's unique identifier
|
||||
/// * `lang` - The language for error messages
|
||||
/// * `existing_location_id` - Optional existing location ID to use instead of generating a new one
|
||||
/// * `series_location_id` - The series location ID to link (optional)
|
||||
/// Returns the ID of the created location.
|
||||
pub fn add_location_section(
|
||||
conn: &Connection, user_id: &str, location_name: &str, book_id: &str, lang: Lang,
|
||||
existing_location_id: Option<&str>, series_location_id: Option<&str>,
|
||||
) -> AppResult<String> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let hashed_name: String = hash_element(location_name);
|
||||
let encrypted_name: String = encrypt_data_with_user_key(location_name, &user_key)?;
|
||||
let location_id: String = create_unique_id(existing_location_id);
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
repo::insert_location(conn, user_id, &location_id, book_id, &encrypted_name, &hashed_name, last_update, series_location_id, lang)
|
||||
}
|
||||
|
||||
/// Adds a new element to a location.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `location_id` - The parent location's unique identifier
|
||||
/// * `element_name` - The name of the element to create
|
||||
/// * `lang` - The language for error messages
|
||||
/// * `existing_element_id` - Optional existing element ID to use instead of generating a new one
|
||||
/// Returns the ID of the created element.
|
||||
pub fn add_location_element(
|
||||
conn: &Connection, user_id: &str, location_id: &str, element_name: &str, lang: Lang,
|
||||
existing_element_id: Option<&str>,
|
||||
) -> AppResult<String> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let hashed_name: String = hash_element(element_name);
|
||||
let encrypted_name: String = encrypt_data_with_user_key(element_name, &user_key)?;
|
||||
let element_id: String = create_unique_id(existing_element_id);
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
repo::insert_location_element(conn, user_id, &element_id, location_id, &encrypted_name, &hashed_name, last_update, lang)
|
||||
}
|
||||
|
||||
/// Adds a new sub-element to a location element.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `element_id` - The parent element's unique identifier
|
||||
/// * `sub_element_name` - The name of the sub-element to create
|
||||
/// * `lang` - The language for error messages
|
||||
/// * `existing_sub_element_id` - Optional existing sub-element ID to use instead of generating a new one
|
||||
/// Returns the ID of the created sub-element.
|
||||
pub fn add_location_sub_element(
|
||||
conn: &Connection, user_id: &str, element_id: &str, sub_element_name: &str, lang: Lang,
|
||||
existing_sub_element_id: Option<&str>,
|
||||
) -> AppResult<String> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let hashed_name: String = hash_element(sub_element_name);
|
||||
let encrypted_name: String = encrypt_data_with_user_key(sub_element_name, &user_key)?;
|
||||
let sub_element_id: String = create_unique_id(existing_sub_element_id);
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
repo::insert_location_sub_element(conn, user_id, &sub_element_id, element_id, &encrypted_name, &hashed_name, last_update, lang)
|
||||
}
|
||||
|
||||
/// Updates multiple location sections along with their elements and sub-elements.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `locations` - Array of location properties to update
|
||||
/// * `lang` - The language for response messages
|
||||
/// Returns an object indicating success and a localized message.
|
||||
pub fn update_location_section(
|
||||
conn: &Connection, user_id: &str, locations: &[LocationProps], lang: Lang,
|
||||
) -> AppResult<(bool, String)> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
for location in locations {
|
||||
let hashed_location_name: String = hash_element(&location.name);
|
||||
let encrypted_location_name: String = encrypt_data_with_user_key(&location.name, &user_key)?;
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
repo::update_location_section(conn, user_id, &location.id, &encrypted_location_name, &hashed_location_name, last_update, lang)?;
|
||||
|
||||
for element in &location.elements {
|
||||
let hashed_element_name: String = hash_element(&element.name);
|
||||
let encrypted_element_name: String = encrypt_data_with_user_key(&element.name, &user_key)?;
|
||||
let encrypted_element_description: String = if element.description.is_empty() { String::new() } else { encrypt_data_with_user_key(&element.description, &user_key)? };
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
repo::update_location_element(conn, user_id, &element.id, &encrypted_element_name, &hashed_element_name, &encrypted_element_description, last_update, lang)?;
|
||||
|
||||
for sub_element in &element.sub_elements {
|
||||
let hashed_sub_element_name: String = hash_element(&sub_element.name);
|
||||
let encrypted_sub_element_name: String = encrypt_data_with_user_key(&sub_element.name, &user_key)?;
|
||||
let encrypted_sub_element_description: String = if sub_element.description.is_empty() { String::new() } else { encrypt_data_with_user_key(&sub_element.description, &user_key)? };
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
repo::update_location_sub_element(conn, user_id, &sub_element.id, &encrypted_sub_element_name, &hashed_sub_element_name, &encrypted_sub_element_description, last_update, lang)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let message: String = if lang == Lang::Fr { "Les sections ont été mis à jour.".to_string() } else { "Sections have been updated.".to_string() };
|
||||
Ok((true, message))
|
||||
}
|
||||
|
||||
/// Updates a location section with optional name change and series link.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `section_id` - The unique identifier of the section
|
||||
/// * `section_name` - The new name (optional)
|
||||
/// * `series_location_id` - The series location ID to link (optional, None to unlink)
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the update was successful.
|
||||
pub fn update_section_with_series_link(
|
||||
conn: &Connection, user_id: &str, section_id: &str, section_name: Option<&str>,
|
||||
series_location_id: Option<&str>, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let encrypted_name: Option<String> = if let Some(name) = section_name {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
Some(encrypt_data_with_user_key(name, &user_key)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let original_name_hash: Option<String> = section_name.map(hash_element);
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
repo::update_section_with_series_link(conn, user_id, section_id, encrypted_name.as_deref(), original_name_hash.as_deref(), series_location_id, last_update, lang)
|
||||
}
|
||||
|
||||
/// Deletes a location section and all its associated elements and sub-elements.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `book_id` - The book's unique identifier
|
||||
/// * `location_id` - The location's unique identifier to delete
|
||||
/// * `deleted_at` - The timestamp of deletion
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the deletion was successful.
|
||||
pub fn delete_location_section(conn: &Connection, user_id: &str, book_id: &str, location_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
|
||||
let deleted: bool = repo::delete_location_section(conn, user_id, location_id, lang)?;
|
||||
if deleted {
|
||||
tombstone_repo::insert(conn, location_id, "book_location", location_id, Some(book_id), user_id, deleted_at, lang)?;
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
/// Deletes a location element and all its associated sub-elements.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `book_id` - The book's unique identifier
|
||||
/// * `element_id` - The element's unique identifier to delete
|
||||
/// * `deleted_at` - The timestamp of deletion
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the deletion was successful.
|
||||
pub fn delete_location_element(conn: &Connection, user_id: &str, book_id: &str, element_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
|
||||
let deleted: bool = repo::delete_location_element(conn, user_id, element_id, lang)?;
|
||||
if deleted {
|
||||
tombstone_repo::insert(conn, element_id, "location_element", element_id, Some(book_id), user_id, deleted_at, lang)?;
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
/// Deletes a location sub-element.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `book_id` - The book's unique identifier
|
||||
/// * `sub_element_id` - The sub-element's unique identifier to delete
|
||||
/// * `deleted_at` - The timestamp of deletion
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the deletion was successful.
|
||||
pub fn delete_location_sub_element(conn: &Connection, user_id: &str, book_id: &str, sub_element_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
|
||||
let deleted: bool = repo::delete_location_sub_element(conn, user_id, sub_element_id, lang)?;
|
||||
if deleted {
|
||||
tombstone_repo::insert(conn, sub_element_id, "location_sub_element", sub_element_id, Some(book_id), user_id, deleted_at, lang)?;
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
/// Retrieves location tags (elements or sub-elements) for tagging purposes.
|
||||
/// Returns sub-elements when an element has multiple sub-elements, otherwise returns the element itself.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `book_id` - The book's unique identifier
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns an array of sub-elements suitable for tagging.
|
||||
pub fn get_location_tags(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<SubElement>> {
|
||||
let tag_records: Vec<repo::LocationElementQueryResult> = repo::fetch_location_tags(conn, user_id, book_id, lang)?;
|
||||
if tag_records.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
let mut element_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
|
||||
for record in &tag_records {
|
||||
*element_counts.entry(record.element_id.clone()).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
let mut sub_elements: Vec<SubElement> = Vec::new();
|
||||
let mut processed_ids: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
|
||||
for record in &tag_records {
|
||||
let element_count: usize = *element_counts.get(&record.element_id).unwrap_or(&0);
|
||||
|
||||
if element_count > 1 {
|
||||
if let Some(ref sub_element_id) = record.sub_element_id {
|
||||
if processed_ids.contains(sub_element_id) {
|
||||
continue;
|
||||
}
|
||||
let decrypted_name: String = decrypt_data_with_user_key(record.sub_elem_name.as_deref().unwrap_or(""), &user_key)?;
|
||||
let decrypted_description: String = if let Some(ref sub_elem_description) = record.sub_elem_description {
|
||||
decrypt_data_with_user_key(sub_elem_description, &user_key)?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
sub_elements.push(SubElement {
|
||||
id: sub_element_id.clone(),
|
||||
name: decrypted_name,
|
||||
description: decrypted_description,
|
||||
});
|
||||
processed_ids.insert(sub_element_id.clone());
|
||||
}
|
||||
} else if element_count == 1 && record.sub_element_id.is_none() {
|
||||
if processed_ids.contains(&record.element_id) {
|
||||
continue;
|
||||
}
|
||||
let decrypted_name: String = decrypt_data_with_user_key(&record.element_name, &user_key)?;
|
||||
let decrypted_description: String = if let Some(ref element_description) = record.element_description {
|
||||
decrypt_data_with_user_key(element_description, &user_key)?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
sub_elements.push(SubElement {
|
||||
id: record.element_id.clone(),
|
||||
name: decrypted_name,
|
||||
description: decrypted_description,
|
||||
});
|
||||
processed_ids.insert(record.element_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(sub_elements)
|
||||
}
|
||||
|
||||
/// Retrieves location elements filtered by specific tag IDs.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The user's unique identifier
|
||||
/// * `locations` - Array of location tag IDs to filter by
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns an array of elements with their associated sub-elements.
|
||||
pub fn get_locations_by_tags(conn: &Connection, user_id: &str, locations: &[String], lang: Lang) -> AppResult<Vec<Element>> {
|
||||
let location_tag_records: Vec<repo::LocationByTagResult> = repo::fetch_locations_by_tags(conn, user_id, locations, lang)?;
|
||||
if location_tag_records.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let mut location_elements: Vec<Element> = Vec::new();
|
||||
|
||||
for record in &location_tag_records {
|
||||
let element_index: Option<usize> = location_elements.iter().position(|elem| elem.name == record.element_name);
|
||||
|
||||
let element_idx: usize = match element_index {
|
||||
Some(idx) => idx,
|
||||
None => {
|
||||
let decrypted_name: String = decrypt_data_with_user_key(&record.element_name, &user_key)?;
|
||||
let decrypted_description: String = if let Some(ref element_description) = record.element_description {
|
||||
decrypt_data_with_user_key(element_description, &user_key)?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
location_elements.push(Element {
|
||||
id: String::new(),
|
||||
name: decrypted_name,
|
||||
description: decrypted_description,
|
||||
sub_elements: vec![],
|
||||
});
|
||||
location_elements.len() - 1
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ref sub_elem_name) = record.sub_elem_name {
|
||||
let sub_element_exists: bool = location_elements[element_idx].sub_elements.iter().any(|sub| sub.name == *sub_elem_name);
|
||||
if !sub_element_exists {
|
||||
let decrypted_name: String = decrypt_data_with_user_key(sub_elem_name, &user_key)?;
|
||||
let decrypted_description: String = if let Some(ref sub_elem_description) = record.sub_elem_description {
|
||||
decrypt_data_with_user_key(sub_elem_description, &user_key)?
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
location_elements[element_idx].sub_elements.push(SubElement {
|
||||
id: String::new(),
|
||||
name: decrypted_name,
|
||||
description: decrypted_description,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(location_elements)
|
||||
}
|
||||
|
||||
/// Generates a formatted description string from an array of location elements.
|
||||
/// * `locations` - Array of location elements to describe
|
||||
/// Returns a formatted string with location names and descriptions.
|
||||
pub fn locations_description(locations: &[Element]) -> String {
|
||||
locations
|
||||
.iter()
|
||||
.map(|location| {
|
||||
let mut description_fields: Vec<String> = Vec::new();
|
||||
if !location.name.is_empty() {
|
||||
description_fields.push(format!("Nom : {}", location.name));
|
||||
}
|
||||
if !location.description.is_empty() {
|
||||
description_fields.push(format!("Description : {}", location.description));
|
||||
}
|
||||
description_fields.join("\n")
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n\n")
|
||||
}
|
||||
27
src-tauri/src/domains/mod.rs
Normal file
27
src-tauri/src/domains/mod.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
pub mod act;
|
||||
pub mod book;
|
||||
pub mod chapter;
|
||||
pub mod chapter_content;
|
||||
pub mod character;
|
||||
pub mod cover;
|
||||
pub mod download;
|
||||
pub mod export;
|
||||
pub mod guideline;
|
||||
pub mod incident;
|
||||
pub mod offline;
|
||||
pub mod issue;
|
||||
pub mod location;
|
||||
pub mod plotpoint;
|
||||
pub mod series;
|
||||
pub mod series_character;
|
||||
pub mod series_location;
|
||||
pub mod series_spell;
|
||||
pub mod series_sync;
|
||||
pub mod series_world;
|
||||
pub mod spell;
|
||||
pub mod spell_tag;
|
||||
pub mod sync;
|
||||
pub mod tombstone;
|
||||
pub mod upload;
|
||||
pub mod user;
|
||||
pub mod world;
|
||||
124
src-tauri/src/domains/offline/commands.rs
Normal file
124
src-tauri/src/domains/offline/commands.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::State;
|
||||
|
||||
use crate::crypto::key_manager;
|
||||
use crate::db::connection::DbManager;
|
||||
use crate::db::schema;
|
||||
use crate::error::AppError;
|
||||
use crate::shared::session::SessionState;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PinData {
|
||||
pub pin: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OfflineResult {
|
||||
pub success: bool,
|
||||
pub error: Option<String>,
|
||||
pub user_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OfflineModeStatus {
|
||||
pub enabled: bool,
|
||||
pub sync_interval: i64,
|
||||
pub has_pin: bool,
|
||||
pub last_user_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OfflineModeData {
|
||||
pub enabled: bool,
|
||||
pub sync_interval_days: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncCheckResult {
|
||||
pub should_sync: bool,
|
||||
pub days_since_sync: Option<i64>,
|
||||
pub sync_interval: Option<i64>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn offline_pin_set(data: PinData, session: State<SessionState>) -> Result<OfflineResult, AppError> {
|
||||
let session_guard = session.lock().map_err(|e| AppError::Internal(format!("Session lock failed: {}", e)))?;
|
||||
let user_id = session_guard.get_user_id().map_err(|e| AppError::Auth(e))?.to_string();
|
||||
drop(session_guard);
|
||||
|
||||
let hashed_pin: String = bcrypt::hash(&data.pin, 10)
|
||||
.map_err(|e| AppError::Encryption(format!("Failed to hash PIN: {}", e)))?;
|
||||
|
||||
key_manager::set_pin_hash(&user_id, &hashed_pin)?;
|
||||
|
||||
Ok(OfflineResult { success: true, error: None, user_id: None })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn offline_pin_verify(data: PinData, db: State<DbManager>, session: State<SessionState>) -> Result<OfflineResult, AppError> {
|
||||
let last_user_id: Option<String> = key_manager::get_last_user_id()?;
|
||||
let last_user_id: String = match last_user_id {
|
||||
Some(id) => id,
|
||||
None => return Ok(OfflineResult { success: false, error: Some("No offline account found".to_string()), user_id: None }),
|
||||
};
|
||||
|
||||
let hashed_pin: Option<String> = key_manager::get_pin_hash(&last_user_id)?;
|
||||
let hashed_pin: String = match hashed_pin {
|
||||
Some(hash) => hash,
|
||||
None => return Ok(OfflineResult { success: false, error: Some("No PIN configured".to_string()), user_id: None }),
|
||||
};
|
||||
|
||||
let is_valid: bool = bcrypt::verify(&data.pin, &hashed_pin)
|
||||
.map_err(|e| AppError::Encryption(format!("Failed to verify PIN: {}", e)))?;
|
||||
|
||||
if !is_valid {
|
||||
return Ok(OfflineResult { success: false, error: Some("Invalid PIN".to_string()), user_id: None });
|
||||
}
|
||||
|
||||
let has_key: bool = key_manager::has_user_encryption_key(&last_user_id);
|
||||
if !has_key {
|
||||
return Ok(OfflineResult { success: false, error: Some("No encryption key found".to_string()), user_id: None });
|
||||
}
|
||||
|
||||
let mut db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
db_manager.initialize(&last_user_id)?;
|
||||
let conn = db_manager.get_connection(&last_user_id)?;
|
||||
schema::run_migrations(conn)?;
|
||||
|
||||
let mut session_guard = session.lock().map_err(|e| AppError::Internal(format!("Session lock failed: {}", e)))?;
|
||||
session_guard.user_id = Some(last_user_id.clone());
|
||||
|
||||
Ok(OfflineResult { success: true, error: None, user_id: Some(last_user_id) })
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn offline_mode_get() -> Result<OfflineModeStatus, AppError> {
|
||||
let last_user_id: Option<String> = key_manager::get_last_user_id()?;
|
||||
let has_pin: bool = match &last_user_id {
|
||||
Some(id) => key_manager::get_pin_hash(id)?.is_some(),
|
||||
None => false,
|
||||
};
|
||||
|
||||
Ok(OfflineModeStatus {
|
||||
enabled: false,
|
||||
sync_interval: 30,
|
||||
has_pin,
|
||||
last_user_id,
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn offline_mode_set(_data: OfflineModeData) -> Result<bool, AppError> {
|
||||
// Offline mode preferences are handled by the frontend/keyring
|
||||
// This is a no-op placeholder that the frontend can call
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn offline_sync_check() -> Result<SyncCheckResult, AppError> {
|
||||
Ok(SyncCheckResult { should_sync: false, days_since_sync: None, sync_interval: None })
|
||||
}
|
||||
1
src-tauri/src/domains/offline/mod.rs
Normal file
1
src-tauri/src/domains/offline/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod commands;
|
||||
2
src-tauri/src/domains/plotpoint/mod.rs
Normal file
2
src-tauri/src/domains/plotpoint/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub mod repo;
|
||||
pub mod service;
|
||||
264
src-tauri/src/domains/plotpoint/repo.rs
Normal file
264
src-tauri/src/domains/plotpoint/repo.rs
Normal file
@@ -0,0 +1,264 @@
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
pub struct BookPlotPointsTable {
|
||||
pub plot_point_id: String,
|
||||
pub title: String,
|
||||
pub hashed_title: String,
|
||||
pub summary: Option<String>,
|
||||
pub linked_incident_id: Option<String>,
|
||||
pub author_id: String,
|
||||
pub book_id: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedPlotPointResult {
|
||||
pub plot_point_id: String,
|
||||
pub book_id: String,
|
||||
pub title: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct PlotPointQuery {
|
||||
pub plot_point_id: String,
|
||||
pub title: String,
|
||||
pub summary: String,
|
||||
pub linked_incident_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Fetches all plot points for a specific book.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `book_id` - The ID of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of plot points with their basic information.
|
||||
/// Errors if the plot points cannot be retrieved.
|
||||
pub fn fetch_all_plot_points(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<PlotPointQuery>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT plot_point_id, title, summary, linked_incident_id FROM book_plot_points WHERE author_id=?1 AND book_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les points d'intrigue.".to_string() } else { "Unable to retrieve plot points.".to_string() }))?;
|
||||
|
||||
let plot_points = statement
|
||||
.query_map(params![user_id, book_id], |query_row| {
|
||||
Ok(PlotPointQuery {
|
||||
plot_point_id: query_row.get(0)?,
|
||||
title: query_row.get(1)?,
|
||||
summary: query_row.get::<_, Option<String>>(2)?.unwrap_or_default(),
|
||||
linked_incident_id: query_row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les points d'intrigue.".to_string() } else { "Unable to retrieve plot points.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les points d'intrigue.".to_string() } else { "Unable to retrieve plot points.".to_string() }))?;
|
||||
|
||||
Ok(plot_points)
|
||||
}
|
||||
|
||||
/// Inserts a new plot point into the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `plot_point_id` - The unique ID for the new plot point
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `book_id` - The ID of the book
|
||||
/// * `encrypted_name` - The encrypted title of the plot point
|
||||
/// * `hashed_name` - The hashed title for duplicate checking
|
||||
/// * `incident_id` - The ID of the linked incident (can be empty string)
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the ID of the newly created plot point.
|
||||
/// Errors if the plot point already exists or cannot be added.
|
||||
pub fn insert_new_plot_point(
|
||||
conn: &Connection, plot_point_id: &str, user_id: &str, book_id: &str,
|
||||
encrypted_name: &str, hashed_name: &str, incident_id: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<String> {
|
||||
let existing_plot_point: Option<String> = conn
|
||||
.query_row("SELECT plot_point_id FROM book_plot_points WHERE author_id=?1 AND book_id=?2 AND hashed_title=?3", params![user_id, book_id, hashed_name], |query_row| query_row.get(0))
|
||||
.optional()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du point d'intrigue.".to_string() } else { "Unable to verify plot point existence.".to_string() }))?;
|
||||
|
||||
if existing_plot_point.is_some() {
|
||||
return Err(AppError::Validation(if lang == Lang::Fr { "Ce point de l'intrigue existe déjà.".to_string() } else { "This plot point already exists.".to_string() }));
|
||||
}
|
||||
|
||||
let insert_result = conn
|
||||
.execute("INSERT INTO book_plot_points (plot_point_id,title,hashed_title,author_id,book_id,linked_incident_id,last_update) VALUES (?1,?2,?3,?4,?5,?6,?7)", params![plot_point_id, encrypted_name, hashed_name, user_id, book_id, incident_id, last_update])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le point d'intrigue.".to_string() } else { "Unable to add plot point.".to_string() }))?;
|
||||
|
||||
if insert_result == 0 {
|
||||
return Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de l'ajout du point d'intrigue.".to_string() } else { "Error adding plot point.".to_string() }));
|
||||
}
|
||||
|
||||
Ok(plot_point_id.to_string())
|
||||
}
|
||||
|
||||
/// Deletes a plot point from the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `plot_point_id` - The ID of the plot point to delete
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the plot point was deleted, false otherwise.
|
||||
/// Errors if the plot point cannot be deleted.
|
||||
pub fn delete_plot_point(conn: &Connection, user_id: &str, plot_point_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let delete_result = conn
|
||||
.execute("DELETE FROM book_plot_points WHERE author_id=?1 AND plot_point_id=?2", params![user_id, plot_point_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le point d'intrigue.".to_string() } else { "Unable to delete plot point.".to_string() }))?;
|
||||
|
||||
Ok(delete_result > 0)
|
||||
}
|
||||
|
||||
/// Updates an existing plot point in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `book_id` - The ID of the book
|
||||
/// * `plot_point_id` - The ID of the plot point to update
|
||||
/// * `encrypted_plot_point_name` - The new encrypted title
|
||||
/// * `plot_point_hashed_name` - The new hashed title
|
||||
/// * `plot_point_summary` - The new summary
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the plot point was updated, false otherwise.
|
||||
/// Errors if the update fails.
|
||||
pub fn update_plot_point(
|
||||
conn: &Connection, user_id: &str, book_id: &str, plot_point_id: &str,
|
||||
encrypted_plot_point_name: &str, plot_point_hashed_name: &str, plot_point_summary: &str,
|
||||
last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute("UPDATE book_plot_points SET title=?1, hashed_title=?2, summary=?3, last_update=?4 WHERE author_id=?5 AND book_id=?6 AND plot_point_id=?7", params![encrypted_plot_point_name, plot_point_hashed_name, plot_point_summary, last_update, user_id, book_id, plot_point_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour le point d'intrigue.".to_string() } else { "Unable to update plot point.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Fetches all plot points for a book with complete information for synchronization.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `book_id` - The ID of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of complete plot point records.
|
||||
/// Errors if the plot points cannot be retrieved.
|
||||
pub fn fetch_book_plot_points(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<BookPlotPointsTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT plot_point_id, title, hashed_title, summary, linked_incident_id, author_id, book_id, last_update FROM book_plot_points WHERE author_id=?1 AND book_id=?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les points d'intrigue.".to_string() } else { "Unable to retrieve plot points.".to_string() }))?;
|
||||
|
||||
let plot_points = statement
|
||||
.query_map(params![user_id, book_id], |query_row| {
|
||||
Ok(BookPlotPointsTable {
|
||||
plot_point_id: query_row.get(0)?,
|
||||
title: query_row.get(1)?,
|
||||
hashed_title: query_row.get(2)?,
|
||||
summary: query_row.get(3)?,
|
||||
linked_incident_id: query_row.get(4)?,
|
||||
author_id: query_row.get(5)?,
|
||||
book_id: query_row.get(6)?,
|
||||
last_update: query_row.get(7)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les points d'intrigue.".to_string() } else { "Unable to retrieve plot points.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les points d'intrigue.".to_string() } else { "Unable to retrieve plot points.".to_string() }))?;
|
||||
|
||||
Ok(plot_points)
|
||||
}
|
||||
|
||||
/// Fetches all synced plot points for a user across all books.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of synced plot point records with minimal information.
|
||||
/// Errors if the synced plot points cannot be retrieved.
|
||||
pub fn fetch_synced_plot_points(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedPlotPointResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT plot_point_id, book_id, title, last_update FROM book_plot_points WHERE author_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les points d'intrigue synchronisés.".to_string() } else { "Unable to retrieve synced plot points.".to_string() }))?;
|
||||
|
||||
let synced_plot_points = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedPlotPointResult {
|
||||
plot_point_id: query_row.get(0)?,
|
||||
book_id: query_row.get(1)?,
|
||||
title: query_row.get(2)?,
|
||||
last_update: query_row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les points d'intrigue synchronisés.".to_string() } else { "Unable to retrieve synced plot points.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les points d'intrigue synchronisés.".to_string() } else { "Unable to retrieve synced plot points.".to_string() }))?;
|
||||
|
||||
Ok(synced_plot_points)
|
||||
}
|
||||
|
||||
/// Inserts a plot point during synchronization from remote data.
|
||||
/// * `conn` - Database connection
|
||||
/// * `plot_point_id` - The unique ID of the plot point
|
||||
/// * `title` - The encrypted title
|
||||
/// * `hashed_title` - The hashed title for duplicate checking
|
||||
/// * `summary` - The encrypted summary (can be null)
|
||||
/// * `linked_incident_id` - The ID of the linked incident (can be null)
|
||||
/// * `author_id` - The ID of the author
|
||||
/// * `book_id` - The ID of the book
|
||||
/// * `last_update` - The timestamp of the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the plot point was inserted, false otherwise.
|
||||
/// Errors if the plot point cannot be inserted.
|
||||
pub fn insert_sync_plot_point(
|
||||
conn: &Connection, plot_point_id: &str, title: &str, hashed_title: &str,
|
||||
summary: Option<&str>, linked_incident_id: Option<&str>, author_id: &str,
|
||||
book_id: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute("INSERT INTO book_plot_points (plot_point_id, title, hashed_title, summary, linked_incident_id, author_id, book_id, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", params![plot_point_id, title, hashed_title, summary, linked_incident_id, author_id, book_id, last_update])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer le point d'intrigue.".to_string() } else { "Unable to insert plot point.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Fetches complete plot point data by its ID.
|
||||
/// * `conn` - Database connection
|
||||
/// * `plot_point_id` - The ID of the plot point to retrieve
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array containing the plot point data (empty if not found).
|
||||
/// Errors if the plot point cannot be retrieved.
|
||||
pub fn fetch_complete_plot_point_by_id(conn: &Connection, plot_point_id: &str, lang: Lang) -> AppResult<Vec<BookPlotPointsTable>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT plot_point_id, title, hashed_title, summary, linked_incident_id, author_id, book_id, last_update FROM book_plot_points WHERE plot_point_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le point d'intrigue complet.".to_string() } else { "Unable to retrieve complete plot point.".to_string() }))?;
|
||||
|
||||
let plot_point = statement
|
||||
.query_map(params![plot_point_id], |query_row| {
|
||||
Ok(BookPlotPointsTable {
|
||||
plot_point_id: query_row.get(0)?,
|
||||
title: query_row.get(1)?,
|
||||
hashed_title: query_row.get(2)?,
|
||||
summary: query_row.get(3)?,
|
||||
linked_incident_id: query_row.get(4)?,
|
||||
author_id: query_row.get(5)?,
|
||||
book_id: query_row.get(6)?,
|
||||
last_update: query_row.get(7)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le point d'intrigue complet.".to_string() } else { "Unable to retrieve complete plot point.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer le point d'intrigue complet.".to_string() } else { "Unable to retrieve complete plot point.".to_string() }))?;
|
||||
|
||||
Ok(plot_point)
|
||||
}
|
||||
|
||||
/// Checks if a plot point exists in the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The ID of the user/author
|
||||
/// * `book_id` - The ID of the book
|
||||
/// * `plot_point_id` - The ID of the plot point to check
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the plot point exists, false otherwise.
|
||||
/// Errors if the existence check fails.
|
||||
pub fn plot_point_exist(conn: &Connection, user_id: &str, book_id: &str, plot_point_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let existing_plot_point: Option<i32> = conn
|
||||
.query_row("SELECT 1 FROM book_plot_points WHERE author_id =?1 AND book_id =?2 AND plot_point_id =?3", params![user_id, book_id, plot_point_id], |query_row| query_row.get(0))
|
||||
.optional()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence du point de intrigue.".to_string() } else { "Unable to check plot point existence.".to_string() }))?;
|
||||
|
||||
Ok(existing_plot_point.is_some())
|
||||
}
|
||||
130
src-tauri/src/domains/plotpoint/service.rs
Normal file
130
src-tauri/src/domains/plotpoint/service.rs
Normal file
@@ -0,0 +1,130 @@
|
||||
use rusqlite::Connection;
|
||||
|
||||
use crate::crypto::encryption::{decrypt_data_with_user_key, encrypt_data_with_user_key, hash_element};
|
||||
use crate::crypto::key_manager::get_user_encryption_key;
|
||||
use crate::domains::chapter::repo::ActChapterQuery;
|
||||
use crate::domains::plotpoint::repo;
|
||||
use crate::domains::tombstone::repo as tombstone_repo;
|
||||
use crate::error::AppResult;
|
||||
use crate::helpers::{create_unique_id, timestamp_in_seconds};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
/// Represents the story details associated with a plot point.
|
||||
pub struct PlotPointStory {
|
||||
pub plot_title: String,
|
||||
pub plot_summary: String,
|
||||
pub chapter_summary: String,
|
||||
pub chapter_goal: String,
|
||||
}
|
||||
|
||||
/// Represents a plot point with its properties and associated chapters.
|
||||
pub struct PlotPointProps {
|
||||
pub plot_point_id: String,
|
||||
pub title: String,
|
||||
pub summary: String,
|
||||
pub linked_incident_id: Option<String>,
|
||||
pub chapters: Vec<ActChapterQuery>,
|
||||
}
|
||||
|
||||
/// Represents a synced plot point with minimal information.
|
||||
pub struct SyncedPlotPoint {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
/// Retrieves all plot points for a specific book with their associated chapters.
|
||||
/// Decrypts plot point titles and summaries using the user's encryption key.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `act_chapters` - Array of act chapters to associate with plot points
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns a list of plot point properties with their associated chapters.
|
||||
/// Errors if the plot points cannot be retrieved or decrypted.
|
||||
pub fn get_plot_points(
|
||||
conn: &Connection, user_id: &str, book_id: &str,
|
||||
act_chapters: &[ActChapterQuery], lang: Lang,
|
||||
) -> AppResult<Vec<PlotPointProps>> {
|
||||
let plot_point_query_results: Vec<repo::PlotPointQuery> = repo::fetch_all_plot_points(conn, user_id, book_id, lang)?;
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
let mut plot_points: Vec<PlotPointProps> = Vec::new();
|
||||
|
||||
if !plot_point_query_results.is_empty() {
|
||||
for plot_point_row in &plot_point_query_results {
|
||||
let mut associated_chapters: Vec<ActChapterQuery> = Vec::new();
|
||||
|
||||
for chapter in act_chapters {
|
||||
if chapter.plot_point_id.as_deref() == Some(&plot_point_row.plot_point_id) {
|
||||
associated_chapters.push(ActChapterQuery {
|
||||
chapter_info_id: chapter.chapter_info_id,
|
||||
chapter_id: chapter.chapter_id.clone(),
|
||||
title: chapter.title.clone(),
|
||||
chapter_order: chapter.chapter_order,
|
||||
act_id: chapter.act_id,
|
||||
incident_id: chapter.incident_id.clone(),
|
||||
plot_point_id: chapter.plot_point_id.clone(),
|
||||
summary: chapter.summary.clone(),
|
||||
goal: chapter.goal.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let decrypted_title: String = if !plot_point_row.title.is_empty() { decrypt_data_with_user_key(&plot_point_row.title, &user_encryption_key)? } else { String::new() };
|
||||
let decrypted_summary: String = if !plot_point_row.summary.is_empty() { decrypt_data_with_user_key(&plot_point_row.summary, &user_encryption_key)? } else { String::new() };
|
||||
|
||||
plot_points.push(PlotPointProps {
|
||||
plot_point_id: plot_point_row.plot_point_id.clone(),
|
||||
title: decrypted_title,
|
||||
summary: decrypted_summary,
|
||||
linked_incident_id: plot_point_row.linked_incident_id.clone(),
|
||||
chapters: associated_chapters,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(plot_points)
|
||||
}
|
||||
|
||||
/// Creates a new plot point for a book, encrypting the name before storage.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `incident_id` - The identifier of the linked incident
|
||||
/// * `name` - The name/title of the plot point
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// * `existing_plot_point_id` - Optional existing plot point ID to use instead of generating a new one
|
||||
/// Returns the unique identifier of the created plot point.
|
||||
/// Errors if the plot point already exists or cannot be added.
|
||||
pub fn add_new_plot_point(
|
||||
conn: &Connection, user_id: &str, book_id: &str, incident_id: &str,
|
||||
name: &str, lang: Lang, existing_plot_point_id: Option<&str>,
|
||||
) -> AppResult<String> {
|
||||
let user_encryption_key: String = get_user_encryption_key(user_id)?;
|
||||
let encrypted_name: String = encrypt_data_with_user_key(name, &user_encryption_key)?;
|
||||
let hashed_name: String = hash_element(name);
|
||||
let plot_point_id: String = create_unique_id(existing_plot_point_id);
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
|
||||
repo::insert_new_plot_point(conn, &plot_point_id, user_id, book_id, &encrypted_name, &hashed_name, incident_id, last_update, lang)
|
||||
}
|
||||
|
||||
/// Removes a plot point from the database.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `plot_id` - The unique identifier of the plot point to remove
|
||||
/// * `deleted_at` - The timestamp of deletion
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the plot point was successfully deleted, false otherwise.
|
||||
/// Errors if the plot point cannot be deleted.
|
||||
pub fn remove_plot_point(
|
||||
conn: &Connection, user_id: &str, book_id: &str, plot_id: &str,
|
||||
deleted_at: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let deleted: bool = repo::delete_plot_point(conn, user_id, plot_id, lang)?;
|
||||
if deleted {
|
||||
tombstone_repo::insert(conn, book_id, "book_plot_points", plot_id, None, user_id, deleted_at, lang)?;
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
164
src-tauri/src/domains/series/commands.rs
Normal file
164
src-tauri/src/domains/series/commands.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
use serde::Deserialize;
|
||||
use tauri::State;
|
||||
|
||||
use crate::db::connection::DbManager;
|
||||
use crate::domains::series::service;
|
||||
use crate::error::AppError;
|
||||
use crate::shared::session::SessionState;
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
fn get_session(session: &State<SessionState>) -> Result<(String, Lang), AppError> {
|
||||
let session_guard = session.lock().map_err(|e| AppError::Internal(format!("Session lock failed: {}", e)))?;
|
||||
let user_id = session_guard.get_user_id().map_err(|e| AppError::Auth(e))?.to_string();
|
||||
let lang = session_guard.lang;
|
||||
Ok((user_id, lang))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_series_list(db: State<DbManager>, session: State<SessionState>) -> Result<Vec<service::SeriesListItemProps>, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_series_list(conn, &user_id, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetSeriesDetailData {
|
||||
pub series_id: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_series_detail(data: GetSeriesDetailData, db: State<DbManager>, session: State<SessionState>) -> Result<service::SeriesDetailProps, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_series_detail(conn, &user_id, &data.series_id, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateSeriesData {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub book_ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_series(data: CreateSeriesData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::create_series(conn, &user_id, &data.name, &data.description, lang, data.book_ids.as_deref())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateSeriesData {
|
||||
pub series_id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_series(data: UpdateSeriesData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::update_series(conn, &user_id, &data.series_id, &data.name, &data.description, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteSeriesData {
|
||||
pub series_id: String,
|
||||
pub deleted_at: i64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_series(data: DeleteSeriesData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::delete_series(conn, &user_id, &data.series_id, data.deleted_at, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetSeriesBooksData {
|
||||
pub series_id: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_series_books(data: GetSeriesBooksData, db: State<DbManager>, session: State<SessionState>) -> Result<Vec<service::SeriesBookProps>, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_series_books(conn, &user_id, &data.series_id, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddBookToSeriesData {
|
||||
pub series_id: String,
|
||||
pub book_id: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_book_to_series(data: AddBookToSeriesData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
let books = service::get_series_books(conn, &user_id, &data.series_id, lang)?;
|
||||
let next_order = books.len() as i64 + 1;
|
||||
service::add_book_to_series(conn, &user_id, &data.series_id, &data.book_id, next_order, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RemoveBookFromSeriesData {
|
||||
pub series_id: String,
|
||||
pub book_id: String,
|
||||
pub deleted_at: i64,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_book_from_series(data: RemoveBookFromSeriesData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::remove_book_from_series(conn, &user_id, &data.series_id, &data.book_id, data.deleted_at, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ReorderSeriesBooksData {
|
||||
pub series_id: String,
|
||||
pub book_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn reorder_series_books(data: ReorderSeriesBooksData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
let books_order: Vec<service::BooksOrderPost> = data.book_ids.into_iter().enumerate().map(|(index, book_id)| service::BooksOrderPost {
|
||||
book_id,
|
||||
order: (index + 1) as i64,
|
||||
}).collect();
|
||||
service::update_books_order(conn, &user_id, &data.series_id, &books_order, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetSeriesForBookData {
|
||||
pub book_id: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_series_for_book(data: GetSeriesForBookData, db: State<DbManager>, session: State<SessionState>) -> Result<Option<String>, AppError> {
|
||||
let (_user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&_user_id)?;
|
||||
service::get_series_id_for_book(conn, &data.book_id, lang)
|
||||
}
|
||||
3
src-tauri/src/domains/series/mod.rs
Normal file
3
src-tauri/src/domains/series/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod commands;
|
||||
pub mod repo;
|
||||
pub mod service;
|
||||
530
src-tauri/src/domains/series/repo.rs
Normal file
530
src-tauri/src/domains/series/repo.rs
Normal file
@@ -0,0 +1,530 @@
|
||||
use rusqlite::{params, Connection};
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
pub struct SeriesResult {
|
||||
pub series_id: String,
|
||||
pub user_id: String,
|
||||
pub name: String,
|
||||
pub hashed_name: String,
|
||||
pub description: Option<String>,
|
||||
pub cover_image: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SeriesBookResult {
|
||||
pub series_id: String,
|
||||
pub book_id: String,
|
||||
pub book_order: i64,
|
||||
pub title: String,
|
||||
pub cover_image: Option<String>,
|
||||
}
|
||||
|
||||
pub struct SeriesListItem {
|
||||
pub series_id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub cover_image: Option<String>,
|
||||
pub book_count: i64,
|
||||
pub book_ids: Option<String>,
|
||||
}
|
||||
|
||||
pub struct SeriesTableResult {
|
||||
pub series_id: String,
|
||||
pub user_id: String,
|
||||
pub name: String,
|
||||
pub hashed_name: String,
|
||||
pub description: Option<String>,
|
||||
pub cover_image: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SeriesBooksTableResult {
|
||||
pub series_id: String,
|
||||
pub book_id: String,
|
||||
pub book_order: i64,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedSeriesResult {
|
||||
pub series_id: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedSeriesBookResult {
|
||||
pub series_id: String,
|
||||
pub book_id: String,
|
||||
pub book_order: i64,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct BookOrderItem {
|
||||
pub book_id: String,
|
||||
pub order: i64,
|
||||
}
|
||||
|
||||
/// Fetches all series for a user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of series with book counts.
|
||||
pub fn fetch_user_series(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SeriesListItem>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT series.series_id, series.name, series.description, series.cover_image, COUNT(series_books.book_id) AS book_count, GROUP_CONCAT(series_books.book_id) AS book_ids FROM book_series series LEFT JOIN series_books ON series.series_id = series_books.series_id WHERE series.user_id = ?1 GROUP BY series.series_id, series.last_update ORDER BY series.last_update DESC")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les séries.".to_string() } else { "Unable to retrieve series.".to_string() }))?;
|
||||
|
||||
let series = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SeriesListItem {
|
||||
series_id: query_row.get(0)?, name: query_row.get(1)?,
|
||||
description: query_row.get(2)?, cover_image: query_row.get(3)?,
|
||||
book_count: query_row.get(4)?, book_ids: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les séries.".to_string() } else { "Unable to retrieve series.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les séries.".to_string() } else { "Unable to retrieve series.".to_string() }))?;
|
||||
|
||||
Ok(series)
|
||||
}
|
||||
|
||||
/// Fetches a single series by its ID.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the series result or None if not found.
|
||||
pub fn fetch_series_by_id(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<Option<SeriesResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT series_id, user_id, name, hashed_name, description, cover_image, last_update FROM book_series WHERE series_id = ?1 AND user_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la série.".to_string() } else { "Unable to retrieve series.".to_string() }))?;
|
||||
|
||||
let series = statement
|
||||
.query_row(params![series_id, user_id], |query_row| {
|
||||
Ok(SeriesResult {
|
||||
series_id: query_row.get(0)?, user_id: query_row.get(1)?,
|
||||
name: query_row.get(2)?, hashed_name: query_row.get(3)?,
|
||||
description: query_row.get(4)?, cover_image: query_row.get(5)?,
|
||||
last_update: query_row.get(6)?,
|
||||
})
|
||||
});
|
||||
|
||||
match series {
|
||||
Ok(row) => Ok(Some(row)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la série.".to_string() } else { "Unable to retrieve series.".to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a new series.
|
||||
/// * `conn` - Database connection
|
||||
/// * `series_id` - The unique identifier for the new series
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `name` - The encrypted name
|
||||
/// * `hashed_name` - The hashed name for duplicate detection
|
||||
/// * `description` - The encrypted description (nullable)
|
||||
/// * `last_update` - The creation timestamp
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the series ID if successful.
|
||||
pub fn insert_series(
|
||||
conn: &Connection, series_id: &str, user_id: &str, name: &str, hashed_name: &str,
|
||||
description: Option<&str>, last_update: i64, lang: Lang,
|
||||
) -> AppResult<String> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO book_series (series_id, user_id, name, hashed_name, description, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![series_id, user_id, name, hashed_name, description, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de créer la série.".to_string() } else { "Unable to create series.".to_string() }))?;
|
||||
|
||||
if insert_result > 0 {
|
||||
Ok(series_id.to_string())
|
||||
} else {
|
||||
Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de la création de la série.".to_string() } else { "Error creating series.".to_string() }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates an existing series.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `name` - The encrypted name
|
||||
/// * `hashed_name` - The hashed name
|
||||
/// * `description` - The encrypted description (nullable)
|
||||
/// * `last_update` - The update timestamp
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the update was successful.
|
||||
pub fn update_series(
|
||||
conn: &Connection, user_id: &str, series_id: &str, name: &str, hashed_name: &str,
|
||||
description: Option<&str>, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute(
|
||||
"UPDATE book_series SET name = ?1, hashed_name = ?2, description = ?3, last_update = ?4 WHERE series_id = ?5 AND user_id = ?6",
|
||||
params![name, hashed_name, description, last_update, series_id, user_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour la série.".to_string() } else { "Unable to update series.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Deletes a series.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the deletion was successful.
|
||||
pub fn delete_series(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let delete_result = conn
|
||||
.execute("DELETE FROM book_series WHERE series_id = ?1 AND user_id = ?2", params![series_id, user_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer la série.".to_string() } else { "Unable to delete series.".to_string() }))?;
|
||||
|
||||
Ok(delete_result > 0)
|
||||
}
|
||||
|
||||
/// Fetches all books in a series with their order.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of books in the series.
|
||||
pub fn fetch_series_books(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesBookResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT sb.series_id, sb.book_id, sb.book_order, b.title, b.cover_image FROM series_books sb INNER JOIN erit_books b ON sb.book_id = b.book_id WHERE sb.series_id = ?1 AND b.author_id = ?2 ORDER BY sb.book_order")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les livres de la série.".to_string() } else { "Unable to retrieve series books.".to_string() }))?;
|
||||
|
||||
let books = statement
|
||||
.query_map(params![series_id, user_id], |query_row| {
|
||||
Ok(SeriesBookResult {
|
||||
series_id: query_row.get(0)?, book_id: query_row.get(1)?,
|
||||
book_order: query_row.get(2)?, title: query_row.get(3)?,
|
||||
cover_image: query_row.get(4)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les livres de la série.".to_string() } else { "Unable to retrieve series books.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les livres de la série.".to_string() } else { "Unable to retrieve series books.".to_string() }))?;
|
||||
|
||||
Ok(books)
|
||||
}
|
||||
|
||||
/// Adds a book to a series.
|
||||
/// * `conn` - Database connection
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `book_order` - The order of the book in the series
|
||||
/// * `last_update` - The timestamp
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the addition was successful.
|
||||
pub fn add_book_to_series(conn: &Connection, series_id: &str, book_id: &str, book_order: i64, last_update: i64, lang: Lang) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO series_books (series_id, book_id, book_order, last_update) VALUES (?1, ?2, ?3, ?4) ON CONFLICT(series_id, book_id) DO UPDATE SET book_order = excluded.book_order, last_update = excluded.last_update",
|
||||
params![series_id, book_id, book_order, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le livre à la série.".to_string() } else { "Unable to add book to series.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Removes a book from a series.
|
||||
/// * `conn` - Database connection
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the removal was successful.
|
||||
pub fn remove_book_from_series(conn: &Connection, series_id: &str, book_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let delete_result = conn
|
||||
.execute("DELETE FROM series_books WHERE series_id = ?1 AND book_id = ?2", params![series_id, book_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de retirer le livre de la série.".to_string() } else { "Unable to remove book from series.".to_string() }))?;
|
||||
|
||||
Ok(delete_result > 0)
|
||||
}
|
||||
|
||||
/// Updates the order of books in a series.
|
||||
/// * `conn` - Database connection
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `books_order` - An array of BookOrderItem with book_id and order
|
||||
/// * `last_update` - The timestamp
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the update was successful.
|
||||
pub fn update_books_order(conn: &Connection, series_id: &str, books_order: &[BookOrderItem], last_update: i64, lang: Lang) -> AppResult<bool> {
|
||||
for book_order in books_order {
|
||||
conn.execute(
|
||||
"UPDATE series_books SET book_order = ?1, last_update = ?2 WHERE series_id = ?3 AND book_id = ?4",
|
||||
params![book_order.order, last_update, series_id, book_order.book_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de réordonner les livres.".to_string() } else { "Unable to reorder books.".to_string() }))?;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Checks if a series exists for a user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the series exists.
|
||||
pub fn is_series_exist(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT 1 FROM book_series WHERE series_id = ?1 AND user_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de la série.".to_string() } else { "Unable to check series existence.".to_string() }))?;
|
||||
|
||||
let exists = statement
|
||||
.query_row(params![series_id, user_id], |_query_row| Ok(true));
|
||||
|
||||
match exists {
|
||||
Ok(_) => Ok(true),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false),
|
||||
Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier l'existence de la série.".to_string() } else { "Unable to check series existence.".to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets the series ID for a book if it belongs to one.
|
||||
/// * `conn` - Database connection
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the series ID or None.
|
||||
pub fn get_series_id_for_book(conn: &Connection, book_id: &str, lang: Lang) -> AppResult<Option<String>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT series_id FROM series_books WHERE book_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier la série du livre.".to_string() } else { "Unable to check book series.".to_string() }))?;
|
||||
|
||||
let result = statement
|
||||
.query_row(params![book_id], |query_row| query_row.get::<_, String>(0));
|
||||
|
||||
match result {
|
||||
Ok(series_id) => Ok(Some(series_id)),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
|
||||
Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier la série du livre.".to_string() } else { "Unable to check book series.".to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetches a series table row for sync purposes.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array containing the series table row.
|
||||
pub fn fetch_series_table_for_sync(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesTableResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT series_id, user_id, name, hashed_name, description, cover_image, last_update FROM book_series WHERE series_id = ?1 AND user_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la série pour sync.".to_string() } else { "Unable to retrieve series for sync.".to_string() }))?;
|
||||
|
||||
let series = statement
|
||||
.query_map(params![series_id, user_id], |query_row| {
|
||||
Ok(SeriesTableResult {
|
||||
series_id: query_row.get(0)?, user_id: query_row.get(1)?,
|
||||
name: query_row.get(2)?, hashed_name: query_row.get(3)?,
|
||||
description: query_row.get(4)?, cover_image: query_row.get(5)?,
|
||||
last_update: query_row.get(6)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la série pour sync.".to_string() } else { "Unable to retrieve series for sync.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la série pour sync.".to_string() } else { "Unable to retrieve series for sync.".to_string() }))?;
|
||||
|
||||
Ok(series)
|
||||
}
|
||||
|
||||
/// Fetches all series-books relationships for sync.
|
||||
/// * `conn` - Database connection
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of series-books table rows.
|
||||
pub fn fetch_series_books_table(conn: &Connection, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesBooksTableResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT series_id, book_id, book_order, last_update FROM series_books WHERE series_id = ?1 ORDER BY book_order")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les livres de la série pour sync.".to_string() } else { "Unable to retrieve series books for sync.".to_string() }))?;
|
||||
|
||||
let books = statement
|
||||
.query_map(params![series_id], |query_row| {
|
||||
Ok(SeriesBooksTableResult {
|
||||
series_id: query_row.get(0)?, book_id: query_row.get(1)?,
|
||||
book_order: query_row.get(2)?, last_update: query_row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les livres de la série pour sync.".to_string() } else { "Unable to retrieve series books for sync.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les livres de la série pour sync.".to_string() } else { "Unable to retrieve series books for sync.".to_string() }))?;
|
||||
|
||||
Ok(books)
|
||||
}
|
||||
|
||||
/// Fetches all series for a user for sync comparison.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of synced series results.
|
||||
pub fn fetch_synced_series(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedSeriesResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT series_id, name, description, last_update FROM book_series WHERE user_id = ?1 ORDER BY last_update DESC")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les séries pour sync.".to_string() } else { "Unable to retrieve series for sync.".to_string() }))?;
|
||||
|
||||
let series = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedSeriesResult {
|
||||
series_id: query_row.get(0)?, name: query_row.get(1)?,
|
||||
description: query_row.get(2)?, last_update: query_row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les séries pour sync.".to_string() } else { "Unable to retrieve series for sync.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les séries pour sync.".to_string() } else { "Unable to retrieve series for sync.".to_string() }))?;
|
||||
|
||||
Ok(series)
|
||||
}
|
||||
|
||||
/// Fetches all series-books relationships for a user for sync comparison.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of synced series book results.
|
||||
pub fn fetch_synced_series_books(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedSeriesBookResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT sb.series_id, sb.book_id, sb.book_order, sb.last_update FROM series_books sb INNER JOIN book_series bs ON sb.series_id = bs.series_id WHERE bs.user_id = ?1 ORDER BY sb.book_order")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les livres de séries pour sync.".to_string() } else { "Unable to retrieve series books for sync.".to_string() }))?;
|
||||
|
||||
let books = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedSeriesBookResult {
|
||||
series_id: query_row.get(0)?, book_id: query_row.get(1)?,
|
||||
book_order: query_row.get(2)?, last_update: query_row.get(3)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les livres de séries pour sync.".to_string() } else { "Unable to retrieve series books for sync.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer les livres de séries pour sync.".to_string() } else { "Unable to retrieve series books for sync.".to_string() }))?;
|
||||
|
||||
Ok(books)
|
||||
}
|
||||
|
||||
/// Fetches a complete series by ID for sync.
|
||||
/// * `conn` - Database connection
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array containing the series.
|
||||
pub fn fetch_complete_series_by_id(conn: &Connection, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesTableResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT series_id, user_id, name, hashed_name, description, cover_image, last_update FROM book_series WHERE series_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la série complète.".to_string() } else { "Unable to retrieve complete series.".to_string() }))?;
|
||||
|
||||
let series = statement
|
||||
.query_map(params![series_id], |query_row| {
|
||||
Ok(SeriesTableResult {
|
||||
series_id: query_row.get(0)?, user_id: query_row.get(1)?,
|
||||
name: query_row.get(2)?, hashed_name: query_row.get(3)?,
|
||||
description: query_row.get(4)?, cover_image: query_row.get(5)?,
|
||||
last_update: query_row.get(6)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la série complète.".to_string() } else { "Unable to retrieve complete series.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de récupérer la série complète.".to_string() } else { "Unable to retrieve complete series.".to_string() }))?;
|
||||
|
||||
Ok(series)
|
||||
}
|
||||
|
||||
/// Inserts a series for sync purposes.
|
||||
/// * `conn` - Database connection
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `name` - The encrypted name
|
||||
/// * `hashed_name` - The hashed name
|
||||
/// * `description` - The encrypted description (nullable)
|
||||
/// * `cover_image` - The cover image (nullable)
|
||||
/// * `last_update` - The sync timestamp
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the insertion was successful.
|
||||
pub fn insert_sync_series(
|
||||
conn: &Connection, series_id: &str, user_id: &str, name: &str, hashed_name: &str,
|
||||
description: Option<&str>, cover_image: Option<&str>, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO book_series (series_id, user_id, name, hashed_name, description, cover_image, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) ON CONFLICT(series_id) DO UPDATE SET name = excluded.name, hashed_name = excluded.hashed_name, description = excluded.description, cover_image = excluded.cover_image, last_update = excluded.last_update",
|
||||
params![series_id, user_id, name, hashed_name, description, cover_image, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer la série pour sync.".to_string() } else { "Unable to insert series for sync.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Updates a series for sync purposes.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `name` - The encrypted name
|
||||
/// * `hashed_name` - The hashed name
|
||||
/// * `description` - The encrypted description (nullable)
|
||||
/// * `cover_image` - The cover image (nullable)
|
||||
/// * `last_update` - The sync timestamp
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the update was successful.
|
||||
pub fn update_sync_series(
|
||||
conn: &Connection, user_id: &str, series_id: &str, name: &str, hashed_name: &str,
|
||||
description: Option<&str>, cover_image: Option<&str>, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute(
|
||||
"UPDATE book_series SET name = ?1, hashed_name = ?2, description = ?3, cover_image = ?4, last_update = ?5 WHERE series_id = ?6 AND user_id = ?7",
|
||||
params![name, hashed_name, description, cover_image, last_update, series_id, user_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre à jour la série pour sync.".to_string() } else { "Unable to update series for sync.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Inserts a series-book relationship for sync.
|
||||
/// * `conn` - Database connection
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `book_order` - The order of the book
|
||||
/// * `last_update` - The sync timestamp
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the insertion was successful.
|
||||
pub fn insert_sync_series_book(conn: &Connection, series_id: &str, book_id: &str, book_order: i64, last_update: i64, lang: Lang) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO series_books (series_id, book_id, book_order, last_update) VALUES (?1, ?2, ?3, ?4) ON CONFLICT(series_id, book_id) DO UPDATE SET book_order = excluded.book_order, last_update = excluded.last_update",
|
||||
params![series_id, book_id, book_order, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'insérer la liaison série-livre pour sync.".to_string() } else { "Unable to insert series-book for sync.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Checks if a series-book relationship exists.
|
||||
/// * `conn` - Database connection
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the relationship exists.
|
||||
pub fn is_series_book_exist(conn: &Connection, series_id: &str, book_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT 1 FROM series_books WHERE series_id = ?1 AND book_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier la liaison série-livre.".to_string() } else { "Unable to check series-book.".to_string() }))?;
|
||||
|
||||
let exists = statement
|
||||
.query_row(params![series_id, book_id], |_query_row| Ok(true));
|
||||
|
||||
match exists {
|
||||
Ok(_) => Ok(true),
|
||||
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(false),
|
||||
Err(_) => Err(AppError::Internal(if lang == Lang::Fr { "Impossible de vérifier la liaison série-livre.".to_string() } else { "Unable to check series-book.".to_string() })),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if a series exists for a user (alias for is_series_exist).
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the series exists.
|
||||
pub fn series_exists(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
is_series_exist(conn, user_id, series_id, lang)
|
||||
}
|
||||
299
src-tauri/src/domains/series/service.rs
Normal file
299
src-tauri/src/domains/series/service.rs
Normal file
@@ -0,0 +1,299 @@
|
||||
use rusqlite::Connection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::crypto::encryption::{encrypt_data_with_user_key, decrypt_data_with_user_key, hash_element};
|
||||
use crate::crypto::key_manager::get_user_encryption_key;
|
||||
use crate::domains::series::repo;
|
||||
use crate::domains::tombstone::repo as tombstone_repo;
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::helpers::{create_unique_id, timestamp_in_seconds};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesProps {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub cover_image: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesDetailProps {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub cover_image: Option<String>,
|
||||
pub books: Vec<SeriesBookProps>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesBookProps {
|
||||
pub book_id: String,
|
||||
pub title: String,
|
||||
pub order: i64,
|
||||
pub cover_image: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesListItemProps {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub cover_image: Option<String>,
|
||||
pub book_count: i64,
|
||||
pub book_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BooksOrderPost {
|
||||
pub book_id: String,
|
||||
pub order: i64,
|
||||
}
|
||||
|
||||
/// Gets the list of all series for a user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the list of series with decrypted names and descriptions.
|
||||
pub fn get_series_list(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SeriesListItemProps>> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let series_results: Vec<repo::SeriesListItem> = repo::fetch_user_series(conn, user_id, lang)?;
|
||||
|
||||
let mut series_list: Vec<SeriesListItemProps> = Vec::with_capacity(series_results.len());
|
||||
for series_item in series_results {
|
||||
let decrypted_name: String = decrypt_data_with_user_key(&series_item.name, &user_key)?;
|
||||
let decrypted_description: String = if let Some(ref description) = series_item.description { decrypt_data_with_user_key(description, &user_key)? } else { String::new() };
|
||||
let book_ids: Vec<String> = if let Some(ref book_ids_str) = series_item.book_ids { book_ids_str.split(',').map(|s| s.to_string()).collect() } else { vec![] };
|
||||
|
||||
series_list.push(SeriesListItemProps {
|
||||
id: series_item.series_id,
|
||||
name: decrypted_name,
|
||||
description: decrypted_description,
|
||||
cover_image: series_item.cover_image,
|
||||
book_count: series_item.book_count,
|
||||
book_ids,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(series_list)
|
||||
}
|
||||
|
||||
/// Gets the detail of a series including its books.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the series detail with decrypted data.
|
||||
pub fn get_series_detail(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<SeriesDetailProps> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
let series_result: Option<repo::SeriesResult> = repo::fetch_series_by_id(conn, user_id, series_id, lang)?;
|
||||
let series_result: repo::SeriesResult = series_result.ok_or_else(|| {
|
||||
AppError::Internal(if lang == Lang::Fr { "Série non trouvée.".to_string() } else { "Series not found.".to_string() })
|
||||
})?;
|
||||
|
||||
let books_result: Vec<repo::SeriesBookResult> = repo::fetch_series_books(conn, user_id, series_id, lang)?;
|
||||
|
||||
let mut books: Vec<SeriesBookProps> = Vec::with_capacity(books_result.len());
|
||||
for book in books_result {
|
||||
let decrypted_title: String = decrypt_data_with_user_key(&book.title, &user_key)?;
|
||||
books.push(SeriesBookProps {
|
||||
book_id: book.book_id,
|
||||
title: decrypted_title,
|
||||
order: book.book_order,
|
||||
cover_image: book.cover_image,
|
||||
});
|
||||
}
|
||||
|
||||
let decrypted_name: String = decrypt_data_with_user_key(&series_result.name, &user_key)?;
|
||||
let decrypted_description: String = if let Some(ref description) = series_result.description { decrypt_data_with_user_key(description, &user_key)? } else { String::new() };
|
||||
|
||||
Ok(SeriesDetailProps {
|
||||
id: series_result.series_id,
|
||||
name: decrypted_name,
|
||||
description: decrypted_description,
|
||||
cover_image: series_result.cover_image,
|
||||
books,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new series.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `name` - The name of the series
|
||||
/// * `description` - The description of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// * `book_ids` - Optional array of book IDs to add to the series
|
||||
/// Returns the created series ID.
|
||||
pub fn create_series(conn: &Connection, user_id: &str, name: &str, description: &str, lang: Lang, book_ids: Option<&[String]>) -> AppResult<String> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let series_id: String = create_unique_id(None);
|
||||
|
||||
let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?;
|
||||
let hashed_name: String = hash_element(name);
|
||||
let encrypted_description: Option<String> = if description.is_empty() { None } else { Some(encrypt_data_with_user_key(description, &user_key)?) };
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
|
||||
repo::insert_series(conn, &series_id, user_id, &encrypted_name, &hashed_name, encrypted_description.as_deref(), last_update, lang)?;
|
||||
|
||||
if let Some(book_ids) = book_ids {
|
||||
for (index, book_id) in book_ids.iter().enumerate() {
|
||||
let book_order: i64 = (index as i64) + 1;
|
||||
repo::add_book_to_series(conn, &series_id, book_id, book_order, last_update, lang)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(series_id)
|
||||
}
|
||||
|
||||
/// Updates an existing series.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `name` - The name of the series
|
||||
/// * `description` - The description of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the update was successful.
|
||||
pub fn update_series(conn: &Connection, user_id: &str, series_id: &str, name: &str, description: &str, lang: Lang) -> AppResult<bool> {
|
||||
let exists: bool = repo::is_series_exist(conn, user_id, series_id, lang)?;
|
||||
if !exists {
|
||||
return Err(AppError::Internal(if lang == Lang::Fr { "Série non trouvée.".to_string() } else { "Series not found.".to_string() }));
|
||||
}
|
||||
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?;
|
||||
let hashed_name: String = hash_element(name);
|
||||
let encrypted_description: Option<String> = if description.is_empty() { None } else { Some(encrypt_data_with_user_key(description, &user_key)?) };
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
|
||||
repo::update_series(conn, user_id, series_id, &encrypted_name, &hashed_name, encrypted_description.as_deref(), last_update, lang)
|
||||
}
|
||||
|
||||
/// Deletes a series.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `deleted_at` - The timestamp of deletion
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the deletion was successful.
|
||||
pub fn delete_series(conn: &Connection, user_id: &str, series_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
|
||||
let exists: bool = repo::is_series_exist(conn, user_id, series_id, lang)?;
|
||||
if !exists {
|
||||
return Err(AppError::Internal(if lang == Lang::Fr { "Série non trouvée.".to_string() } else { "Series not found.".to_string() }));
|
||||
}
|
||||
|
||||
let deleted: bool = repo::delete_series(conn, user_id, series_id, lang)?;
|
||||
if deleted {
|
||||
tombstone_repo::insert(conn, series_id, "book_series", series_id, None, user_id, deleted_at, lang)?;
|
||||
}
|
||||
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
/// Adds a book to a series.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `order` - The order of the book in the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the addition was successful.
|
||||
pub fn add_book_to_series(conn: &Connection, user_id: &str, series_id: &str, book_id: &str, order: i64, lang: Lang) -> AppResult<bool> {
|
||||
let exists: bool = repo::is_series_exist(conn, user_id, series_id, lang)?;
|
||||
if !exists {
|
||||
return Err(AppError::Internal(if lang == Lang::Fr { "Série non trouvée.".to_string() } else { "Series not found.".to_string() }));
|
||||
}
|
||||
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
repo::add_book_to_series(conn, series_id, book_id, order, last_update, lang)
|
||||
}
|
||||
|
||||
/// Removes a book from a series.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `deleted_at` - The timestamp of deletion
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the removal was successful.
|
||||
pub fn remove_book_from_series(conn: &Connection, user_id: &str, series_id: &str, book_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
|
||||
let exists: bool = repo::is_series_exist(conn, user_id, series_id, lang)?;
|
||||
if !exists {
|
||||
return Err(AppError::Internal(if lang == Lang::Fr { "Série non trouvée.".to_string() } else { "Series not found.".to_string() }));
|
||||
}
|
||||
|
||||
let deleted: bool = repo::remove_book_from_series(conn, series_id, book_id, lang)?;
|
||||
if deleted {
|
||||
let entity_id: String = format!("{}_{}", series_id, book_id);
|
||||
tombstone_repo::insert(conn, &entity_id, "series_books", &entity_id, None, user_id, deleted_at, lang)?;
|
||||
}
|
||||
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
/// Updates the order of books in a series.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `books_order` - An array of BooksOrderPost with book_id and order
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the update was successful.
|
||||
pub fn update_books_order(conn: &Connection, user_id: &str, series_id: &str, books_order: &[BooksOrderPost], lang: Lang) -> AppResult<bool> {
|
||||
let exists: bool = repo::is_series_exist(conn, user_id, series_id, lang)?;
|
||||
if !exists {
|
||||
return Err(AppError::Internal(if lang == Lang::Fr { "Série non trouvée.".to_string() } else { "Series not found.".to_string() }));
|
||||
}
|
||||
|
||||
let repo_books_order: Vec<repo::BookOrderItem> = books_order.iter().map(|item| repo::BookOrderItem {
|
||||
book_id: item.book_id.clone(),
|
||||
order: item.order,
|
||||
}).collect();
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
|
||||
repo::update_books_order(conn, series_id, &repo_books_order, last_update, lang)
|
||||
}
|
||||
|
||||
/// Gets the series ID for a book if it belongs to one.
|
||||
/// * `conn` - Database connection
|
||||
/// * `book_id` - The unique identifier of the book
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the series ID or None.
|
||||
pub fn get_series_id_for_book(conn: &Connection, book_id: &str, lang: Lang) -> AppResult<Option<String>> {
|
||||
repo::get_series_id_for_book(conn, book_id, lang)
|
||||
}
|
||||
|
||||
/// Gets only the books of a series (without series details).
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the list of books in the series.
|
||||
pub fn get_series_books(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesBookProps>> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
let exists: bool = repo::is_series_exist(conn, user_id, series_id, lang)?;
|
||||
if !exists {
|
||||
return Err(AppError::Internal(if lang == Lang::Fr { "Série non trouvée.".to_string() } else { "Series not found.".to_string() }));
|
||||
}
|
||||
|
||||
let books_result: Vec<repo::SeriesBookResult> = repo::fetch_series_books(conn, user_id, series_id, lang)?;
|
||||
|
||||
let mut books: Vec<SeriesBookProps> = Vec::with_capacity(books_result.len());
|
||||
for book in books_result {
|
||||
let decrypted_title: String = decrypt_data_with_user_key(&book.title, &user_key)?;
|
||||
books.push(SeriesBookProps {
|
||||
book_id: book.book_id,
|
||||
title: decrypted_title,
|
||||
order: book.book_order,
|
||||
cover_image: book.cover_image,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(books)
|
||||
}
|
||||
108
src-tauri/src/domains/series_character/commands.rs
Normal file
108
src-tauri/src/domains/series_character/commands.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use serde::Deserialize;
|
||||
use tauri::State;
|
||||
|
||||
use crate::db::connection::DbManager;
|
||||
use crate::domains::series_character::service;
|
||||
use crate::error::AppError;
|
||||
use crate::shared::session::SessionState;
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
fn get_session(session: &State<SessionState>) -> Result<(String, Lang), AppError> {
|
||||
let session_guard = session.lock().map_err(|e| AppError::Internal(format!("Session lock failed: {}", e)))?;
|
||||
let user_id = session_guard.get_user_id().map_err(|e| AppError::Auth(e))?.to_string();
|
||||
let lang = session_guard.lang;
|
||||
Ok((user_id, lang))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetSeriesCharacterListData { pub series_id: String }
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_series_character_list(data: GetSeriesCharacterListData, db: State<DbManager>, session: State<SessionState>) -> Result<Vec<service::SeriesCharacterListProps>, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_character_list(conn, &user_id, &data.series_id, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GetSeriesCharacterAttributesData { pub character_id: String }
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_series_character_attributes(data: GetSeriesCharacterAttributesData, db: State<DbManager>, session: State<SessionState>) -> Result<service::CharacterAttributesResponse, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::get_character_attributes(conn, &user_id, &data.character_id, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddSeriesCharacterData {
|
||||
pub series_id: String,
|
||||
pub character: service::SeriesCharacterPropsPost,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_series_character(data: AddSeriesCharacterData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::add_new_character(conn, &user_id, &data.character, &data.series_id, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UpdateSeriesCharacterData {
|
||||
pub character: service::SeriesCharacterPropsPost,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_series_character(data: UpdateSeriesCharacterData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::update_character(conn, &user_id, &data.character, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteSeriesCharacterData { pub character_id: String, pub deleted_at: i64 }
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_series_character(data: DeleteSeriesCharacterData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::delete_character(conn, &user_id, &data.character_id, data.deleted_at, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AddSeriesCharacterAttributeData {
|
||||
pub character_id: String,
|
||||
pub r#type: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn add_series_character_attribute(data: AddSeriesCharacterAttributeData, db: State<DbManager>, session: State<SessionState>) -> Result<String, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::add_new_attribute(conn, &data.character_id, &user_id, &data.r#type, &data.name, lang)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeleteSeriesCharacterAttributeData { pub attribute_id: String, pub deleted_at: i64 }
|
||||
|
||||
#[tauri::command]
|
||||
pub fn delete_series_character_attribute(data: DeleteSeriesCharacterAttributeData, db: State<DbManager>, session: State<SessionState>) -> Result<bool, AppError> {
|
||||
let (user_id, lang) = get_session(&session)?;
|
||||
let db_manager = db.lock().map_err(|e| AppError::Internal(format!("DB lock failed: {}", e)))?;
|
||||
let conn = db_manager.get_connection(&user_id)?;
|
||||
service::delete_attribute(conn, &user_id, &data.attribute_id, data.deleted_at, lang)
|
||||
}
|
||||
3
src-tauri/src/domains/series_character/mod.rs
Normal file
3
src-tauri/src/domains/series_character/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod commands;
|
||||
pub mod repo;
|
||||
pub mod service;
|
||||
746
src-tauri/src/domains/series_character/repo.rs
Normal file
746
src-tauri/src/domains/series_character/repo.rs
Normal file
@@ -0,0 +1,746 @@
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
pub struct SeriesCharacterResult {
|
||||
pub character_id: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
pub nickname: Option<String>,
|
||||
pub age: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub species: Option<String>,
|
||||
pub nationality: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub title: String,
|
||||
pub category: String,
|
||||
pub image: String,
|
||||
pub role: String,
|
||||
pub biography: String,
|
||||
pub history: String,
|
||||
pub speech_pattern: Option<String>,
|
||||
pub catchphrase: Option<String>,
|
||||
pub residence: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
pub struct SeriesCharacterAttributeResult {
|
||||
pub attr_id: String,
|
||||
pub attribute_name: String,
|
||||
pub attribute_value: String,
|
||||
}
|
||||
|
||||
pub struct SeriesCharactersTableResult {
|
||||
pub character_id: String,
|
||||
pub series_id: String,
|
||||
pub user_id: String,
|
||||
pub first_name: String,
|
||||
pub last_name: Option<String>,
|
||||
pub nickname: Option<String>,
|
||||
pub age: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub species: Option<String>,
|
||||
pub nationality: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub category: String,
|
||||
pub image: Option<String>,
|
||||
pub role: Option<String>,
|
||||
pub biography: Option<String>,
|
||||
pub history: Option<String>,
|
||||
pub speech_pattern: Option<String>,
|
||||
pub catchphrase: Option<String>,
|
||||
pub residence: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub color: Option<String>,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SeriesCharacterAttributesTableResult {
|
||||
pub attr_id: String,
|
||||
pub character_id: String,
|
||||
pub user_id: String,
|
||||
pub attribute_name: String,
|
||||
pub attribute_value: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedSeriesCharacterResult {
|
||||
pub character_id: String,
|
||||
pub series_id: String,
|
||||
pub first_name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
pub struct SyncedSeriesCharacterAttributeResult {
|
||||
pub attr_id: String,
|
||||
pub character_id: String,
|
||||
pub attribute_name: String,
|
||||
pub last_update: i64,
|
||||
}
|
||||
|
||||
/// Fetches all characters for a specific series owned by the user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of character results.
|
||||
pub fn fetch_characters(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesCharacterResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT character_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color FROM series_characters WHERE series_id = ?1 AND user_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages de la s\u{00e9}rie.".to_string() } else { "Unable to retrieve series characters.".to_string() }))?;
|
||||
|
||||
let characters = statement
|
||||
.query_map(params![series_id, user_id], |query_row| {
|
||||
Ok(SeriesCharacterResult {
|
||||
character_id: query_row.get(0)?, first_name: query_row.get(1)?,
|
||||
last_name: query_row.get(2)?, nickname: query_row.get(3)?,
|
||||
age: query_row.get(4)?, gender: query_row.get(5)?,
|
||||
species: query_row.get(6)?, nationality: query_row.get(7)?,
|
||||
status: query_row.get(8)?, title: query_row.get(9)?,
|
||||
category: query_row.get(10)?, image: query_row.get(11)?,
|
||||
role: query_row.get(12)?, biography: query_row.get(13)?,
|
||||
history: query_row.get(14)?, speech_pattern: query_row.get(15)?,
|
||||
catchphrase: query_row.get(16)?, residence: query_row.get(17)?,
|
||||
notes: query_row.get(18)?, color: query_row.get(19)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages de la s\u{00e9}rie.".to_string() } else { "Unable to retrieve series characters.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages de la s\u{00e9}rie.".to_string() } else { "Unable to retrieve series characters.".to_string() }))?;
|
||||
|
||||
Ok(characters)
|
||||
}
|
||||
|
||||
/// Adds a new character to a series.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `encrypted_name` - The encrypted first name
|
||||
/// * `encrypted_last_name` - The encrypted last name
|
||||
/// * `encrypted_nickname` - The encrypted nickname
|
||||
/// * `encrypted_age` - The encrypted age
|
||||
/// * `encrypted_gender` - The encrypted gender
|
||||
/// * `encrypted_species` - The encrypted species
|
||||
/// * `encrypted_nationality` - The encrypted nationality
|
||||
/// * `encrypted_status` - The encrypted status
|
||||
/// * `encrypted_title` - The encrypted title
|
||||
/// * `encrypted_category` - The encrypted category
|
||||
/// * `encrypted_image` - The encrypted image
|
||||
/// * `encrypted_role` - The encrypted role
|
||||
/// * `encrypted_biography` - The encrypted biography
|
||||
/// * `encrypted_history` - The encrypted history
|
||||
/// * `encrypted_speech_pattern` - The encrypted speech pattern
|
||||
/// * `encrypted_catchphrase` - The encrypted catchphrase
|
||||
/// * `encrypted_residence` - The encrypted residence
|
||||
/// * `encrypted_notes` - The encrypted notes
|
||||
/// * `encrypted_color` - The encrypted color
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `last_update` - The timestamp for the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the character ID if insertion was successful.
|
||||
pub fn add_new_character(
|
||||
conn: &Connection, user_id: &str, character_id: &str, encrypted_name: &str,
|
||||
encrypted_last_name: Option<&str>, encrypted_nickname: Option<&str>, encrypted_age: Option<&str>,
|
||||
encrypted_gender: Option<&str>, encrypted_species: Option<&str>, encrypted_nationality: Option<&str>,
|
||||
encrypted_status: Option<&str>, encrypted_title: Option<&str>, encrypted_category: Option<&str>,
|
||||
encrypted_image: Option<&str>, encrypted_role: Option<&str>, encrypted_biography: Option<&str>,
|
||||
encrypted_history: Option<&str>, encrypted_speech_pattern: Option<&str>, encrypted_catchphrase: Option<&str>,
|
||||
encrypted_residence: Option<&str>, encrypted_notes: Option<&str>, encrypted_color: Option<&str>,
|
||||
series_id: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<String> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO series_characters (character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23)",
|
||||
params![character_id, series_id, user_id, encrypted_name, encrypted_last_name, encrypted_nickname, encrypted_age, encrypted_gender, encrypted_species, encrypted_nationality, encrypted_status, encrypted_category, encrypted_title, encrypted_image, encrypted_role, encrypted_biography, encrypted_history, encrypted_speech_pattern, encrypted_catchphrase, encrypted_residence, encrypted_notes, encrypted_color, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter le personnage.".to_string() } else { "Unable to add character.".to_string() }))?;
|
||||
|
||||
if insert_result > 0 {
|
||||
Ok(character_id.to_string())
|
||||
} else {
|
||||
Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de l'ajout du personnage.".to_string() } else { "Error adding character.".to_string() }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a new attribute for a series character.
|
||||
/// * `conn` - Database connection
|
||||
/// * `attribute_id` - The unique identifier of the attribute
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `attribute_type` - The attribute type/name
|
||||
/// * `name` - The attribute value
|
||||
/// * `last_update` - The timestamp for the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns the attribute ID if insertion was successful.
|
||||
pub fn insert_attribute(
|
||||
conn: &Connection, attribute_id: &str, character_id: &str, user_id: &str,
|
||||
attribute_type: &str, name: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<String> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO series_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
params![attribute_id, character_id, user_id, attribute_type, name, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ajouter l'attribut.".to_string() } else { "Unable to add attribute.".to_string() }))?;
|
||||
|
||||
if insert_result > 0 {
|
||||
Ok(attribute_id.to_string())
|
||||
} else {
|
||||
Err(AppError::Internal(if lang == Lang::Fr { "Une erreur s'est produite lors de l'ajout de l'attribut.".to_string() } else { "Error adding attribute.".to_string() }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates an existing series character's information.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `encrypted_name` - The encrypted first name
|
||||
/// * `encrypted_last_name` - The encrypted last name
|
||||
/// * `encrypted_nickname` - The encrypted nickname
|
||||
/// * `encrypted_age` - The encrypted age
|
||||
/// * `encrypted_gender` - The encrypted gender
|
||||
/// * `encrypted_species` - The encrypted species
|
||||
/// * `encrypted_nationality` - The encrypted nationality
|
||||
/// * `encrypted_status` - The encrypted status
|
||||
/// * `encrypted_title` - The encrypted title
|
||||
/// * `encrypted_category` - The encrypted category
|
||||
/// * `encrypted_image` - The encrypted image
|
||||
/// * `encrypted_role` - The encrypted role
|
||||
/// * `encrypted_biography` - The encrypted biography
|
||||
/// * `encrypted_history` - The encrypted history
|
||||
/// * `encrypted_speech_pattern` - The encrypted speech pattern
|
||||
/// * `encrypted_catchphrase` - The encrypted catchphrase
|
||||
/// * `encrypted_residence` - The encrypted residence
|
||||
/// * `encrypted_notes` - The encrypted notes
|
||||
/// * `encrypted_color` - The encrypted color
|
||||
/// * `last_update` - The timestamp for the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the update was successful, false otherwise.
|
||||
pub fn update_character(
|
||||
conn: &Connection, user_id: &str, character_id: &str, encrypted_name: &str,
|
||||
encrypted_last_name: Option<&str>, encrypted_nickname: Option<&str>, encrypted_age: Option<&str>,
|
||||
encrypted_gender: Option<&str>, encrypted_species: Option<&str>, encrypted_nationality: Option<&str>,
|
||||
encrypted_status: Option<&str>, encrypted_title: Option<&str>, encrypted_category: Option<&str>,
|
||||
encrypted_image: Option<&str>, encrypted_role: Option<&str>, encrypted_biography: Option<&str>,
|
||||
encrypted_history: Option<&str>, encrypted_speech_pattern: Option<&str>, encrypted_catchphrase: Option<&str>,
|
||||
encrypted_residence: Option<&str>, encrypted_notes: Option<&str>, encrypted_color: Option<&str>,
|
||||
last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute(
|
||||
"UPDATE series_characters SET first_name = ?1, last_name = ?2, nickname = ?3, age = ?4, gender = ?5, species = ?6, nationality = ?7, status = ?8, title = ?9, category = ?10, image = ?11, role = ?12, biography = ?13, history = ?14, speech_pattern = ?15, catchphrase = ?16, residence = ?17, notes = ?18, color = ?19, last_update = ?20 WHERE character_id = ?21 AND user_id = ?22",
|
||||
params![encrypted_name, encrypted_last_name, encrypted_nickname, encrypted_age, encrypted_gender, encrypted_species, encrypted_nationality, encrypted_status, encrypted_title, encrypted_category, encrypted_image, encrypted_role, encrypted_biography, encrypted_history, encrypted_speech_pattern, encrypted_catchphrase, encrypted_residence, encrypted_notes, encrypted_color, last_update, character_id, user_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre \u{00e0} jour le personnage.".to_string() } else { "Unable to update character.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Deletes a series character and all its related data via CASCADE.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the deletion was successful, false otherwise.
|
||||
pub fn delete_character(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
// Delete attributes first
|
||||
conn.execute("DELETE FROM series_characters_attributes WHERE character_id = ?1 AND user_id = ?2", params![character_id, user_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le personnage.".to_string() } else { "Unable to delete character.".to_string() }))?;
|
||||
|
||||
// Delete character
|
||||
let delete_result = conn
|
||||
.execute("DELETE FROM series_characters WHERE character_id = ?1 AND user_id = ?2", params![character_id, user_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer le personnage.".to_string() } else { "Unable to delete character.".to_string() }))?;
|
||||
|
||||
Ok(delete_result > 0)
|
||||
}
|
||||
|
||||
/// Deletes an attribute from a series character.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `attribute_id` - The unique identifier of the attribute
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the deletion was successful, false otherwise.
|
||||
pub fn delete_attribute(conn: &Connection, user_id: &str, attribute_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let delete_result = conn
|
||||
.execute("DELETE FROM series_characters_attributes WHERE attr_id = ?1 AND user_id = ?2", params![attribute_id, user_id])
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de supprimer l'attribut.".to_string() } else { "Unable to delete attribute.".to_string() }))?;
|
||||
|
||||
Ok(delete_result > 0)
|
||||
}
|
||||
|
||||
/// Fetches all attributes for a specific series character.
|
||||
/// * `conn` - Database connection
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of attribute results.
|
||||
pub fn fetch_attributes(conn: &Connection, character_id: &str, user_id: &str, lang: Lang) -> AppResult<Vec<SeriesCharacterAttributeResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT attr_id, attribute_name, attribute_value FROM series_characters_attributes WHERE character_id = ?1 AND user_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs.".to_string() } else { "Unable to retrieve attributes.".to_string() }))?;
|
||||
|
||||
let attributes = statement
|
||||
.query_map(params![character_id, user_id], |query_row| {
|
||||
Ok(SeriesCharacterAttributeResult { attr_id: query_row.get(0)?, attribute_name: query_row.get(1)?, attribute_value: query_row.get(2)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs.".to_string() } else { "Unable to retrieve attributes.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs.".to_string() } else { "Unable to retrieve attributes.".to_string() }))?;
|
||||
|
||||
Ok(attributes)
|
||||
}
|
||||
|
||||
/// Checks if a series character exists.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the character exists, false otherwise.
|
||||
pub fn is_character_exist(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT 1 FROM series_characters WHERE character_id = ?1 AND user_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de v\u{00e9}rifier l'existence du personnage.".to_string() } else { "Unable to check character existence.".to_string() }))?;
|
||||
|
||||
let existence_check = statement
|
||||
.query_row(params![character_id, user_id], |_query_row| Ok(true))
|
||||
.optional()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de v\u{00e9}rifier l'existence du personnage.".to_string() } else { "Unable to check character existence.".to_string() }))?;
|
||||
|
||||
Ok(existence_check.is_some())
|
||||
}
|
||||
|
||||
/// Fetches all characters for a series for sync.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of full character table results.
|
||||
pub fn fetch_series_characters_table(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesCharactersTableResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM series_characters WHERE series_id = ?1 AND user_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages pour sync.".to_string() } else { "Unable to retrieve characters for sync.".to_string() }))?;
|
||||
|
||||
let characters = statement
|
||||
.query_map(params![series_id, user_id], |query_row| {
|
||||
Ok(SeriesCharactersTableResult {
|
||||
character_id: query_row.get(0)?, series_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, first_name: query_row.get(3)?,
|
||||
last_name: query_row.get(4)?, nickname: query_row.get(5)?,
|
||||
age: query_row.get(6)?, gender: query_row.get(7)?,
|
||||
species: query_row.get(8)?, nationality: query_row.get(9)?,
|
||||
status: query_row.get(10)?, title: query_row.get(11)?,
|
||||
category: query_row.get(12)?, image: query_row.get(13)?,
|
||||
role: query_row.get(14)?, biography: query_row.get(15)?,
|
||||
history: query_row.get(16)?, speech_pattern: query_row.get(17)?,
|
||||
catchphrase: query_row.get(18)?, residence: query_row.get(19)?,
|
||||
notes: query_row.get(20)?, color: query_row.get(21)?,
|
||||
last_update: query_row.get(22)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages pour sync.".to_string() } else { "Unable to retrieve characters for sync.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages pour sync.".to_string() } else { "Unable to retrieve characters for sync.".to_string() }))?;
|
||||
|
||||
Ok(characters)
|
||||
}
|
||||
|
||||
/// Fetches all attributes for a character for sync.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of full attribute table results.
|
||||
pub fn fetch_series_character_attributes_table(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult<Vec<SeriesCharacterAttributesTableResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM series_characters_attributes WHERE character_id = ?1 AND user_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs pour sync.".to_string() } else { "Unable to retrieve attributes for sync.".to_string() }))?;
|
||||
|
||||
let attributes = statement
|
||||
.query_map(params![character_id, user_id], |query_row| {
|
||||
Ok(SeriesCharacterAttributesTableResult {
|
||||
attr_id: query_row.get(0)?, character_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, attribute_name: query_row.get(3)?,
|
||||
attribute_value: query_row.get(4)?, last_update: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs pour sync.".to_string() } else { "Unable to retrieve attributes for sync.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs pour sync.".to_string() } else { "Unable to retrieve attributes for sync.".to_string() }))?;
|
||||
|
||||
Ok(attributes)
|
||||
}
|
||||
|
||||
/// Fetches all series characters for a user for sync comparison.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of synced character results.
|
||||
pub fn fetch_synced_series_characters(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedSeriesCharacterResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT character_id, series_id, first_name, last_update FROM series_characters WHERE user_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages de s\u{00e9}rie pour sync.".to_string() } else { "Unable to retrieve series characters for sync.".to_string() }))?;
|
||||
|
||||
let characters = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedSeriesCharacterResult { character_id: query_row.get(0)?, series_id: query_row.get(1)?, first_name: query_row.get(2)?, last_update: query_row.get(3)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages de s\u{00e9}rie pour sync.".to_string() } else { "Unable to retrieve series characters for sync.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages de s\u{00e9}rie pour sync.".to_string() } else { "Unable to retrieve series characters for sync.".to_string() }))?;
|
||||
|
||||
Ok(characters)
|
||||
}
|
||||
|
||||
/// Fetches all series character attributes for a user for sync comparison.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of synced attribute results.
|
||||
pub fn fetch_synced_series_character_attributes(conn: &Connection, user_id: &str, lang: Lang) -> AppResult<Vec<SyncedSeriesCharacterAttributeResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT attr_id, character_id, attribute_name, last_update FROM series_characters_attributes WHERE user_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs de personnage pour sync.".to_string() } else { "Unable to retrieve character attributes for sync.".to_string() }))?;
|
||||
|
||||
let attributes = statement
|
||||
.query_map(params![user_id], |query_row| {
|
||||
Ok(SyncedSeriesCharacterAttributeResult { attr_id: query_row.get(0)?, character_id: query_row.get(1)?, attribute_name: query_row.get(2)?, last_update: query_row.get(3)? })
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs de personnage pour sync.".to_string() } else { "Unable to retrieve character attributes for sync.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs de personnage pour sync.".to_string() } else { "Unable to retrieve character attributes for sync.".to_string() }))?;
|
||||
|
||||
Ok(attributes)
|
||||
}
|
||||
|
||||
/// Fetches a complete character by ID for sync.
|
||||
/// * `conn` - Database connection
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of full character table results.
|
||||
pub fn fetch_complete_character_by_id(conn: &Connection, character_id: &str, lang: Lang) -> AppResult<Vec<SeriesCharactersTableResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM series_characters WHERE character_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer le personnage complet.".to_string() } else { "Unable to retrieve complete character.".to_string() }))?;
|
||||
|
||||
let characters = statement
|
||||
.query_map(params![character_id], |query_row| {
|
||||
Ok(SeriesCharactersTableResult {
|
||||
character_id: query_row.get(0)?, series_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, first_name: query_row.get(3)?,
|
||||
last_name: query_row.get(4)?, nickname: query_row.get(5)?,
|
||||
age: query_row.get(6)?, gender: query_row.get(7)?,
|
||||
species: query_row.get(8)?, nationality: query_row.get(9)?,
|
||||
status: query_row.get(10)?, title: query_row.get(11)?,
|
||||
category: query_row.get(12)?, image: query_row.get(13)?,
|
||||
role: query_row.get(14)?, biography: query_row.get(15)?,
|
||||
history: query_row.get(16)?, speech_pattern: query_row.get(17)?,
|
||||
catchphrase: query_row.get(18)?, residence: query_row.get(19)?,
|
||||
notes: query_row.get(20)?, color: query_row.get(21)?,
|
||||
last_update: query_row.get(22)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer le personnage complet.".to_string() } else { "Unable to retrieve complete character.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer le personnage complet.".to_string() } else { "Unable to retrieve complete character.".to_string() }))?;
|
||||
|
||||
Ok(characters)
|
||||
}
|
||||
|
||||
/// Fetches a complete character attribute by ID for sync.
|
||||
/// * `conn` - Database connection
|
||||
/// * `attr_id` - The unique identifier of the attribute
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of full attribute table results.
|
||||
pub fn fetch_complete_attribute_by_id(conn: &Connection, attr_id: &str, lang: Lang) -> AppResult<Vec<SeriesCharacterAttributesTableResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT attr_id, character_id, user_id, attribute_name, attribute_value, last_update FROM series_characters_attributes WHERE attr_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer l'attribut complet.".to_string() } else { "Unable to retrieve complete attribute.".to_string() }))?;
|
||||
|
||||
let attributes = statement
|
||||
.query_map(params![attr_id], |query_row| {
|
||||
Ok(SeriesCharacterAttributesTableResult {
|
||||
attr_id: query_row.get(0)?, character_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, attribute_name: query_row.get(3)?,
|
||||
attribute_value: query_row.get(4)?, last_update: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer l'attribut complet.".to_string() } else { "Unable to retrieve complete attribute.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer l'attribut complet.".to_string() } else { "Unable to retrieve complete attribute.".to_string() }))?;
|
||||
|
||||
Ok(attributes)
|
||||
}
|
||||
|
||||
/// Inserts a series character for sync.
|
||||
/// * `conn` - Database connection
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `first_name` - The character's first name
|
||||
/// * `last_name` - The character's last name
|
||||
/// * `nickname` - The character's nickname
|
||||
/// * `age` - The character's age
|
||||
/// * `gender` - The character's gender
|
||||
/// * `species` - The character's species
|
||||
/// * `nationality` - The character's nationality
|
||||
/// * `status` - The character's status
|
||||
/// * `category` - The character's category
|
||||
/// * `title` - The character's title
|
||||
/// * `image` - The character's image
|
||||
/// * `role` - The character's role
|
||||
/// * `biography` - The character's biography
|
||||
/// * `history` - The character's history
|
||||
/// * `speech_pattern` - The character's speech pattern
|
||||
/// * `catchphrase` - The character's catchphrase
|
||||
/// * `residence` - The character's residence
|
||||
/// * `notes` - The character's notes
|
||||
/// * `color` - The character's color
|
||||
/// * `last_update` - The timestamp for the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the insertion was successful, false otherwise.
|
||||
pub fn insert_sync_series_character(
|
||||
conn: &Connection, character_id: &str, series_id: &str, user_id: &str, first_name: &str,
|
||||
last_name: Option<&str>, nickname: Option<&str>, age: Option<&str>, gender: Option<&str>,
|
||||
species: Option<&str>, nationality: Option<&str>, status: Option<&str>, category: &str,
|
||||
title: Option<&str>, image: Option<&str>, role: Option<&str>, biography: Option<&str>,
|
||||
history: Option<&str>, speech_pattern: Option<&str>, catchphrase: Option<&str>,
|
||||
residence: Option<&str>, notes: Option<&str>, color: Option<&str>, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO series_characters (character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23) ON CONFLICT(character_id) DO UPDATE SET first_name = excluded.first_name, last_name = excluded.last_name, nickname = excluded.nickname, age = excluded.age, gender = excluded.gender, species = excluded.species, nationality = excluded.nationality, status = excluded.status, category = excluded.category, title = excluded.title, image = excluded.image, role = excluded.role, biography = excluded.biography, history = excluded.history, speech_pattern = excluded.speech_pattern, catchphrase = excluded.catchphrase, residence = excluded.residence, notes = excluded.notes, color = excluded.color, last_update = excluded.last_update",
|
||||
params![character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ins\u{00e9}rer le personnage pour sync.".to_string() } else { "Unable to insert character for sync.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Updates a series character for sync.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `first_name` - The character's first name
|
||||
/// * `last_name` - The character's last name
|
||||
/// * `nickname` - The character's nickname
|
||||
/// * `age` - The character's age
|
||||
/// * `gender` - The character's gender
|
||||
/// * `species` - The character's species
|
||||
/// * `nationality` - The character's nationality
|
||||
/// * `status` - The character's status
|
||||
/// * `category` - The character's category
|
||||
/// * `title` - The character's title
|
||||
/// * `image` - The character's image
|
||||
/// * `role` - The character's role
|
||||
/// * `biography` - The character's biography
|
||||
/// * `history` - The character's history
|
||||
/// * `speech_pattern` - The character's speech pattern
|
||||
/// * `catchphrase` - The character's catchphrase
|
||||
/// * `residence` - The character's residence
|
||||
/// * `notes` - The character's notes
|
||||
/// * `color` - The character's color
|
||||
/// * `last_update` - The timestamp for the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the update was successful, false otherwise.
|
||||
pub fn update_sync_series_character(
|
||||
conn: &Connection, user_id: &str, character_id: &str, first_name: &str,
|
||||
last_name: Option<&str>, nickname: Option<&str>, age: Option<&str>, gender: Option<&str>,
|
||||
species: Option<&str>, nationality: Option<&str>, status: Option<&str>, category: &str,
|
||||
title: Option<&str>, image: Option<&str>, role: Option<&str>, biography: Option<&str>,
|
||||
history: Option<&str>, speech_pattern: Option<&str>, catchphrase: Option<&str>,
|
||||
residence: Option<&str>, notes: Option<&str>, color: Option<&str>, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute(
|
||||
"UPDATE series_characters SET first_name = ?1, last_name = ?2, nickname = ?3, age = ?4, gender = ?5, species = ?6, nationality = ?7, status = ?8, category = ?9, title = ?10, image = ?11, role = ?12, biography = ?13, history = ?14, speech_pattern = ?15, catchphrase = ?16, residence = ?17, notes = ?18, color = ?19, last_update = ?20 WHERE character_id = ?21 AND user_id = ?22",
|
||||
params![first_name, last_name, nickname, age, gender, species, nationality, status, category, title, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update, character_id, user_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre \u{00e0} jour le personnage pour sync.".to_string() } else { "Unable to update character for sync.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Inserts a series character attribute for sync.
|
||||
/// * `conn` - Database connection
|
||||
/// * `attr_id` - The unique identifier of the attribute
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `attribute_name` - The attribute name
|
||||
/// * `attribute_value` - The attribute value
|
||||
/// * `last_update` - The timestamp for the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the insertion was successful, false otherwise.
|
||||
pub fn insert_sync_series_character_attribute(
|
||||
conn: &Connection, attr_id: &str, character_id: &str, user_id: &str,
|
||||
attribute_name: &str, attribute_value: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let insert_result = conn
|
||||
.execute(
|
||||
"INSERT INTO series_characters_attributes (attr_id, character_id, user_id, attribute_name, attribute_value, last_update) VALUES (?1, ?2, ?3, ?4, ?5, ?6) ON CONFLICT(attr_id) DO UPDATE SET attribute_name = excluded.attribute_name, attribute_value = excluded.attribute_value, last_update = excluded.last_update",
|
||||
params![attr_id, character_id, user_id, attribute_name, attribute_value, last_update],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible d'ins\u{00e9}rer l'attribut pour sync.".to_string() } else { "Unable to insert attribute for sync.".to_string() }))?;
|
||||
|
||||
Ok(insert_result > 0)
|
||||
}
|
||||
|
||||
/// Checks if a series character attribute exists.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `attr_id` - The unique identifier of the attribute
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the attribute exists, false otherwise.
|
||||
pub fn is_attribute_exist(conn: &Connection, user_id: &str, attr_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT 1 FROM series_characters_attributes WHERE attr_id = ?1 AND user_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de v\u{00e9}rifier l'existence de l'attribut.".to_string() } else { "Unable to check attribute existence.".to_string() }))?;
|
||||
|
||||
let existence_check = statement
|
||||
.query_row(params![attr_id, user_id], |_query_row| Ok(true))
|
||||
.optional()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de v\u{00e9}rifier l'existence de l'attribut.".to_string() } else { "Unable to check attribute existence.".to_string() }))?;
|
||||
|
||||
Ok(existence_check.is_some())
|
||||
}
|
||||
|
||||
/// Updates a series character attribute for sync.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `attr_id` - The unique identifier of the attribute
|
||||
/// * `attribute_name` - The attribute name
|
||||
/// * `attribute_value` - The attribute value
|
||||
/// * `last_update` - The timestamp for the last update
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the update was successful, false otherwise.
|
||||
pub fn update_sync_series_character_attribute(
|
||||
conn: &Connection, user_id: &str, attr_id: &str, attribute_name: &str,
|
||||
attribute_value: &str, last_update: i64, lang: Lang,
|
||||
) -> AppResult<bool> {
|
||||
let update_result = conn
|
||||
.execute(
|
||||
"UPDATE series_characters_attributes SET attribute_name = ?1, attribute_value = ?2, last_update = ?3 WHERE attr_id = ?4 AND user_id = ?5",
|
||||
params![attribute_name, attribute_value, last_update, attr_id, user_id],
|
||||
)
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de mettre \u{00e0} jour l'attribut pour sync.".to_string() } else { "Unable to update attribute for sync.".to_string() }))?;
|
||||
|
||||
Ok(update_result > 0)
|
||||
}
|
||||
|
||||
/// Fetches all characters for a series for sync (without user filter).
|
||||
/// * `conn` - Database connection
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of full character table results.
|
||||
pub fn fetch_characters_table_for_sync(conn: &Connection, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesCharactersTableResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT character_id, series_id, user_id, first_name, last_name, nickname, age, gender, species, nationality, status, title, category, image, role, biography, history, speech_pattern, catchphrase, residence, notes, color, last_update FROM series_characters WHERE series_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages pour sync.".to_string() } else { "Unable to retrieve characters for sync.".to_string() }))?;
|
||||
|
||||
let characters = statement
|
||||
.query_map(params![series_id], |query_row| {
|
||||
Ok(SeriesCharactersTableResult {
|
||||
character_id: query_row.get(0)?, series_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, first_name: query_row.get(3)?,
|
||||
last_name: query_row.get(4)?, nickname: query_row.get(5)?,
|
||||
age: query_row.get(6)?, gender: query_row.get(7)?,
|
||||
species: query_row.get(8)?, nationality: query_row.get(9)?,
|
||||
status: query_row.get(10)?, title: query_row.get(11)?,
|
||||
category: query_row.get(12)?, image: query_row.get(13)?,
|
||||
role: query_row.get(14)?, biography: query_row.get(15)?,
|
||||
history: query_row.get(16)?, speech_pattern: query_row.get(17)?,
|
||||
catchphrase: query_row.get(18)?, residence: query_row.get(19)?,
|
||||
notes: query_row.get(20)?, color: query_row.get(21)?,
|
||||
last_update: query_row.get(22)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages pour sync.".to_string() } else { "Unable to retrieve characters for sync.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les personnages pour sync.".to_string() } else { "Unable to retrieve characters for sync.".to_string() }))?;
|
||||
|
||||
Ok(characters)
|
||||
}
|
||||
|
||||
/// Fetches all character attributes for a series for sync (without user filter).
|
||||
/// * `conn` - Database connection
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of full attribute table results.
|
||||
pub fn fetch_character_attributes_table_for_sync(conn: &Connection, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesCharacterAttributesTableResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT sca.attr_id, sca.character_id, sca.user_id, sca.attribute_name, sca.attribute_value, sca.last_update FROM series_characters_attributes sca INNER JOIN series_characters sc ON sca.character_id = sc.character_id WHERE sc.series_id = ?1")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs pour sync.".to_string() } else { "Unable to retrieve attributes for sync.".to_string() }))?;
|
||||
|
||||
let attributes = statement
|
||||
.query_map(params![series_id], |query_row| {
|
||||
Ok(SeriesCharacterAttributesTableResult {
|
||||
attr_id: query_row.get(0)?, character_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, attribute_name: query_row.get(3)?,
|
||||
attribute_value: query_row.get(4)?, last_update: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs pour sync.".to_string() } else { "Unable to retrieve attributes for sync.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs pour sync.".to_string() } else { "Unable to retrieve attributes for sync.".to_string() }))?;
|
||||
|
||||
Ok(attributes)
|
||||
}
|
||||
|
||||
/// Fetches all characters for a series (alias for fetch_series_characters_table).
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of full character table results.
|
||||
pub fn fetch_series_characters(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesCharactersTableResult>> {
|
||||
fetch_series_characters_table(conn, user_id, series_id, lang)
|
||||
}
|
||||
|
||||
/// Fetches all character attributes for a series by series ID.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns an array of full attribute table results.
|
||||
pub fn fetch_series_character_attributes_by_series_id(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesCharacterAttributesTableResult>> {
|
||||
let mut statement = conn
|
||||
.prepare("SELECT sca.attr_id, sca.character_id, sca.user_id, sca.attribute_name, sca.attribute_value, sca.last_update FROM series_characters_attributes sca INNER JOIN series_characters sc ON sca.character_id = sc.character_id WHERE sc.series_id = ?1 AND sc.user_id = ?2")
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs par s\u{00e9}rie.".to_string() } else { "Unable to retrieve attributes by series.".to_string() }))?;
|
||||
|
||||
let attributes = statement
|
||||
.query_map(params![series_id, user_id], |query_row| {
|
||||
Ok(SeriesCharacterAttributesTableResult {
|
||||
attr_id: query_row.get(0)?, character_id: query_row.get(1)?,
|
||||
user_id: query_row.get(2)?, attribute_name: query_row.get(3)?,
|
||||
attribute_value: query_row.get(4)?, last_update: query_row.get(5)?,
|
||||
})
|
||||
})
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs par s\u{00e9}rie.".to_string() } else { "Unable to retrieve attributes by series.".to_string() }))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|_| AppError::Internal(if lang == Lang::Fr { "Impossible de r\u{00e9}cup\u{00e9}rer les attributs par s\u{00e9}rie.".to_string() } else { "Unable to retrieve attributes by series.".to_string() }))?;
|
||||
|
||||
Ok(attributes)
|
||||
}
|
||||
|
||||
/// Checks if a series character exists (alias for is_character_exist).
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the character exists, false otherwise.
|
||||
pub fn series_character_exists(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
is_character_exist(conn, user_id, character_id, lang)
|
||||
}
|
||||
|
||||
/// Checks if a series character attribute exists (alias for is_attribute_exist).
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `attr_id` - The unique identifier of the attribute
|
||||
/// * `lang` - The language for error messages ("fr" or "en")
|
||||
/// Returns true if the attribute exists, false otherwise.
|
||||
pub fn series_character_attribute_exists(conn: &Connection, user_id: &str, attr_id: &str, lang: Lang) -> AppResult<bool> {
|
||||
is_attribute_exist(conn, user_id, attr_id, lang)
|
||||
}
|
||||
349
src-tauri/src/domains/series_character/service.rs
Normal file
349
src-tauri/src/domains/series_character/service.rs
Normal file
@@ -0,0 +1,349 @@
|
||||
use rusqlite::Connection;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::crypto::encryption::{encrypt_data_with_user_key, decrypt_data_with_user_key};
|
||||
use crate::crypto::key_manager::get_user_encryption_key;
|
||||
use crate::domains::series_character::repo;
|
||||
use crate::domains::tombstone::repo as tombstone_repo;
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::helpers::{create_unique_id, timestamp_in_seconds};
|
||||
use crate::shared::types::Lang;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesCharacterPropsPost {
|
||||
pub id: Option<String>,
|
||||
pub name: String,
|
||||
pub last_name: String,
|
||||
pub nickname: String,
|
||||
pub age: Option<i64>,
|
||||
pub gender: String,
|
||||
pub species: String,
|
||||
pub nationality: String,
|
||||
pub status: String,
|
||||
pub category: String,
|
||||
pub title: String,
|
||||
pub image: String,
|
||||
pub physical: Vec<NameItem>,
|
||||
pub psychological: Vec<NameItem>,
|
||||
pub relations: Vec<NameItem>,
|
||||
pub skills: Vec<NameItem>,
|
||||
pub weaknesses: Vec<NameItem>,
|
||||
pub strengths: Vec<NameItem>,
|
||||
pub goals: Vec<NameItem>,
|
||||
pub motivations: Vec<NameItem>,
|
||||
pub arc: Vec<NameItem>,
|
||||
pub secrets: Vec<NameItem>,
|
||||
pub fears: Vec<NameItem>,
|
||||
pub flaws: Vec<NameItem>,
|
||||
pub beliefs: Vec<NameItem>,
|
||||
pub conflicts: Vec<NameItem>,
|
||||
pub quotes: Vec<NameItem>,
|
||||
pub distinguishing_marks: Vec<NameItem>,
|
||||
pub items: Vec<NameItem>,
|
||||
pub affiliations: Vec<NameItem>,
|
||||
pub role: String,
|
||||
pub biography: Option<String>,
|
||||
pub history: Option<String>,
|
||||
pub speech_pattern: Option<String>,
|
||||
pub catchphrase: Option<String>,
|
||||
pub residence: Option<String>,
|
||||
pub notes: Option<String>,
|
||||
pub color: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct NameItem {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SeriesCharacterListProps {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub last_name: String,
|
||||
pub nickname: String,
|
||||
pub age: Option<i64>,
|
||||
pub gender: String,
|
||||
pub species: String,
|
||||
pub nationality: String,
|
||||
pub status: String,
|
||||
pub title: String,
|
||||
pub category: String,
|
||||
pub image: String,
|
||||
pub role: String,
|
||||
pub biography: String,
|
||||
pub history: String,
|
||||
pub speech_pattern: String,
|
||||
pub catchphrase: String,
|
||||
pub residence: String,
|
||||
pub notes: String,
|
||||
pub color: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct SeriesAttribute {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CharacterAttributesResponse {
|
||||
pub attributes: Vec<SeriesAttribute>,
|
||||
}
|
||||
|
||||
/// Retrieves a list of characters for a specific series owned by a user.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns a list of decrypted series character properties.
|
||||
pub fn get_character_list(conn: &Connection, user_id: &str, series_id: &str, lang: Lang) -> AppResult<Vec<SeriesCharacterListProps>> {
|
||||
let characters: Vec<repo::SeriesCharacterResult> = repo::fetch_characters(conn, user_id, series_id, lang)?;
|
||||
|
||||
if characters.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
|
||||
let mut character_list: Vec<SeriesCharacterListProps> = Vec::with_capacity(characters.len());
|
||||
for character in characters {
|
||||
let decrypted_name: String = if !character.first_name.is_empty() { decrypt_data_with_user_key(&character.first_name, &user_key)? } else { String::new() };
|
||||
let decrypted_last_name: String = if !character.last_name.is_empty() { decrypt_data_with_user_key(&character.last_name, &user_key)? } else { String::new() };
|
||||
let decrypted_nickname: String = if let Some(ref nickname) = character.nickname { decrypt_data_with_user_key(nickname, &user_key)? } else { String::new() };
|
||||
let decrypted_age: Option<i64> = if let Some(ref age) = character.age { Some(decrypt_data_with_user_key(age, &user_key)?.parse::<i64>().unwrap_or(0)) } else { None };
|
||||
let decrypted_gender: String = if let Some(ref gender) = character.gender { decrypt_data_with_user_key(gender, &user_key)? } else { String::new() };
|
||||
let decrypted_species: String = if let Some(ref species) = character.species { decrypt_data_with_user_key(species, &user_key)? } else { String::new() };
|
||||
let decrypted_nationality: String = if let Some(ref nationality) = character.nationality { decrypt_data_with_user_key(nationality, &user_key)? } else { String::new() };
|
||||
let decrypted_status: String = if let Some(ref status) = character.status { decrypt_data_with_user_key(status, &user_key)? } else { "alive".to_string() };
|
||||
let decrypted_title: String = if !character.title.is_empty() { decrypt_data_with_user_key(&character.title, &user_key)? } else { String::new() };
|
||||
let decrypted_category: String = if !character.category.is_empty() { decrypt_data_with_user_key(&character.category, &user_key)? } else { String::new() };
|
||||
let decrypted_image: String = if !character.image.is_empty() { decrypt_data_with_user_key(&character.image, &user_key)? } else { String::new() };
|
||||
let decrypted_role: String = if !character.role.is_empty() { decrypt_data_with_user_key(&character.role, &user_key)? } else { String::new() };
|
||||
let decrypted_biography: String = if !character.biography.is_empty() { decrypt_data_with_user_key(&character.biography, &user_key)? } else { String::new() };
|
||||
let decrypted_history: String = if !character.history.is_empty() { decrypt_data_with_user_key(&character.history, &user_key)? } else { String::new() };
|
||||
let decrypted_speech_pattern: String = if let Some(ref speech_pattern) = character.speech_pattern { decrypt_data_with_user_key(speech_pattern, &user_key)? } else { String::new() };
|
||||
let decrypted_catchphrase: String = if let Some(ref catchphrase) = character.catchphrase { decrypt_data_with_user_key(catchphrase, &user_key)? } else { String::new() };
|
||||
let decrypted_residence: String = if let Some(ref residence) = character.residence { decrypt_data_with_user_key(residence, &user_key)? } else { String::new() };
|
||||
let decrypted_notes: String = if let Some(ref notes) = character.notes { decrypt_data_with_user_key(notes, &user_key)? } else { String::new() };
|
||||
let decrypted_color: String = if let Some(ref color) = character.color { decrypt_data_with_user_key(color, &user_key)? } else { String::new() };
|
||||
|
||||
character_list.push(SeriesCharacterListProps {
|
||||
id: character.character_id,
|
||||
name: decrypted_name,
|
||||
last_name: decrypted_last_name,
|
||||
nickname: decrypted_nickname,
|
||||
age: decrypted_age,
|
||||
gender: decrypted_gender,
|
||||
species: decrypted_species,
|
||||
nationality: decrypted_nationality,
|
||||
status: decrypted_status,
|
||||
title: decrypted_title,
|
||||
category: decrypted_category,
|
||||
image: decrypted_image,
|
||||
role: decrypted_role,
|
||||
biography: decrypted_biography,
|
||||
history: decrypted_history,
|
||||
speech_pattern: decrypted_speech_pattern,
|
||||
catchphrase: decrypted_catchphrase,
|
||||
residence: decrypted_residence,
|
||||
notes: decrypted_notes,
|
||||
color: decrypted_color,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(character_list)
|
||||
}
|
||||
|
||||
/// Adds a new character to a series with all its attributes.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character` - The character data to create
|
||||
/// * `series_id` - The unique identifier of the series
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the newly created character's ID.
|
||||
pub fn add_new_character(conn: &Connection, user_id: &str, character: &SeriesCharacterPropsPost, series_id: &str, lang: Lang) -> AppResult<String> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let character_id: String = create_unique_id(None);
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
let encrypted_name: String = encrypt_data_with_user_key(&character.name, &user_key)?;
|
||||
let encrypted_last_name: Option<String> = if !character.last_name.is_empty() { Some(encrypt_data_with_user_key(&character.last_name, &user_key)?) } else { None };
|
||||
let encrypted_nickname: Option<String> = if !character.nickname.is_empty() { Some(encrypt_data_with_user_key(&character.nickname, &user_key)?) } else { None };
|
||||
let encrypted_age: Option<String> = if let Some(age) = character.age { Some(encrypt_data_with_user_key(&age.to_string(), &user_key)?) } else { None };
|
||||
let encrypted_gender: Option<String> = if !character.gender.is_empty() { Some(encrypt_data_with_user_key(&character.gender, &user_key)?) } else { None };
|
||||
let encrypted_species: Option<String> = if !character.species.is_empty() { Some(encrypt_data_with_user_key(&character.species, &user_key)?) } else { None };
|
||||
let encrypted_nationality: Option<String> = if !character.nationality.is_empty() { Some(encrypt_data_with_user_key(&character.nationality, &user_key)?) } else { None };
|
||||
let encrypted_status: Option<String> = if !character.status.is_empty() { Some(encrypt_data_with_user_key(&character.status, &user_key)?) } else { None };
|
||||
let encrypted_title: Option<String> = if !character.title.is_empty() { Some(encrypt_data_with_user_key(&character.title, &user_key)?) } else { None };
|
||||
let encrypted_category: Option<String> = if !character.category.is_empty() { Some(encrypt_data_with_user_key(&character.category, &user_key)?) } else { None };
|
||||
let encrypted_image: Option<String> = if !character.image.is_empty() { Some(encrypt_data_with_user_key(&character.image, &user_key)?) } else { None };
|
||||
let encrypted_role: Option<String> = if !character.role.is_empty() { Some(encrypt_data_with_user_key(&character.role, &user_key)?) } else { None };
|
||||
let encrypted_biography: Option<String> = if let Some(ref biography) = character.biography { if !biography.is_empty() { Some(encrypt_data_with_user_key(biography, &user_key)?) } else { None } } else { None };
|
||||
let encrypted_history: Option<String> = if let Some(ref history) = character.history { if !history.is_empty() { Some(encrypt_data_with_user_key(history, &user_key)?) } else { None } } else { None };
|
||||
let encrypted_speech_pattern: Option<String> = if let Some(ref speech_pattern) = character.speech_pattern { if !speech_pattern.is_empty() { Some(encrypt_data_with_user_key(speech_pattern, &user_key)?) } else { None } } else { None };
|
||||
let encrypted_catchphrase: Option<String> = if let Some(ref catchphrase) = character.catchphrase { if !catchphrase.is_empty() { Some(encrypt_data_with_user_key(catchphrase, &user_key)?) } else { None } } else { None };
|
||||
let encrypted_residence: Option<String> = if let Some(ref residence) = character.residence { if !residence.is_empty() { Some(encrypt_data_with_user_key(residence, &user_key)?) } else { None } } else { None };
|
||||
let encrypted_notes: Option<String> = if let Some(ref notes) = character.notes { if !notes.is_empty() { Some(encrypt_data_with_user_key(notes, &user_key)?) } else { None } } else { None };
|
||||
let encrypted_color: Option<String> = if let Some(ref color) = character.color { if !color.is_empty() { Some(encrypt_data_with_user_key(color, &user_key)?) } else { None } } else { None };
|
||||
|
||||
repo::add_new_character(
|
||||
conn, user_id, &character_id, &encrypted_name,
|
||||
encrypted_last_name.as_deref(), encrypted_nickname.as_deref(), encrypted_age.as_deref(),
|
||||
encrypted_gender.as_deref(), encrypted_species.as_deref(), encrypted_nationality.as_deref(),
|
||||
encrypted_status.as_deref(), encrypted_title.as_deref(), encrypted_category.as_deref(),
|
||||
encrypted_image.as_deref(), encrypted_role.as_deref(), encrypted_biography.as_deref(),
|
||||
encrypted_history.as_deref(), encrypted_speech_pattern.as_deref(), encrypted_catchphrase.as_deref(),
|
||||
encrypted_residence.as_deref(), encrypted_notes.as_deref(), encrypted_color.as_deref(),
|
||||
series_id, last_update, lang,
|
||||
)?;
|
||||
|
||||
// Insert array attributes
|
||||
for attribute_item in &character.physical { add_new_attribute(conn, &character_id, user_id, "physical", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.psychological { add_new_attribute(conn, &character_id, user_id, "psychological", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.relations { add_new_attribute(conn, &character_id, user_id, "relations", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.skills { add_new_attribute(conn, &character_id, user_id, "skills", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.weaknesses { add_new_attribute(conn, &character_id, user_id, "weaknesses", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.strengths { add_new_attribute(conn, &character_id, user_id, "strengths", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.goals { add_new_attribute(conn, &character_id, user_id, "goals", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.motivations { add_new_attribute(conn, &character_id, user_id, "motivations", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.arc { add_new_attribute(conn, &character_id, user_id, "arc", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.secrets { add_new_attribute(conn, &character_id, user_id, "secrets", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.fears { add_new_attribute(conn, &character_id, user_id, "fears", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.flaws { add_new_attribute(conn, &character_id, user_id, "flaws", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.beliefs { add_new_attribute(conn, &character_id, user_id, "beliefs", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.conflicts { add_new_attribute(conn, &character_id, user_id, "conflicts", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.quotes { add_new_attribute(conn, &character_id, user_id, "quotes", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.distinguishing_marks { add_new_attribute(conn, &character_id, user_id, "distinguishingMarks", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.items { add_new_attribute(conn, &character_id, user_id, "items", &attribute_item.name, lang)?; }
|
||||
for attribute_item in &character.affiliations { add_new_attribute(conn, &character_id, user_id, "affiliations", &attribute_item.name, lang)?; }
|
||||
|
||||
Ok(character_id)
|
||||
}
|
||||
|
||||
/// Updates an existing character's information and attributes.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character` - The updated character data
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the update was successful.
|
||||
pub fn update_character(conn: &Connection, user_id: &str, character: &SeriesCharacterPropsPost, lang: Lang) -> AppResult<bool> {
|
||||
let character_id: &str = character.id.as_deref().ok_or_else(|| {
|
||||
AppError::Internal(if lang == Lang::Fr { "ID du personnage requis.".to_string() } else { "Character ID required.".to_string() })
|
||||
})?;
|
||||
|
||||
let exists: bool = repo::is_character_exist(conn, user_id, character_id, lang)?;
|
||||
if !exists {
|
||||
return Err(AppError::Internal(if lang == Lang::Fr { "Personnage non trouvé.".to_string() } else { "Character not found.".to_string() }));
|
||||
}
|
||||
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
let encrypted_name: String = encrypt_data_with_user_key(&character.name, &user_key)?;
|
||||
let encrypted_last_name: Option<String> = if !character.last_name.is_empty() { Some(encrypt_data_with_user_key(&character.last_name, &user_key)?) } else { None };
|
||||
let encrypted_nickname: Option<String> = if !character.nickname.is_empty() { Some(encrypt_data_with_user_key(&character.nickname, &user_key)?) } else { None };
|
||||
let encrypted_age: Option<String> = if let Some(age) = character.age { Some(encrypt_data_with_user_key(&age.to_string(), &user_key)?) } else { None };
|
||||
let encrypted_gender: Option<String> = if !character.gender.is_empty() { Some(encrypt_data_with_user_key(&character.gender, &user_key)?) } else { None };
|
||||
let encrypted_species: Option<String> = if !character.species.is_empty() { Some(encrypt_data_with_user_key(&character.species, &user_key)?) } else { None };
|
||||
let encrypted_nationality: Option<String> = if !character.nationality.is_empty() { Some(encrypt_data_with_user_key(&character.nationality, &user_key)?) } else { None };
|
||||
let encrypted_status: Option<String> = if !character.status.is_empty() { Some(encrypt_data_with_user_key(&character.status, &user_key)?) } else { None };
|
||||
let encrypted_title: Option<String> = if !character.title.is_empty() { Some(encrypt_data_with_user_key(&character.title, &user_key)?) } else { None };
|
||||
let encrypted_category: Option<String> = if !character.category.is_empty() { Some(encrypt_data_with_user_key(&character.category, &user_key)?) } else { None };
|
||||
let encrypted_image: Option<String> = if !character.image.is_empty() { Some(encrypt_data_with_user_key(&character.image, &user_key)?) } else { None };
|
||||
let encrypted_role: Option<String> = if !character.role.is_empty() { Some(encrypt_data_with_user_key(&character.role, &user_key)?) } else { None };
|
||||
let encrypted_biography: Option<String> = if let Some(ref biography) = character.biography { if !biography.is_empty() { Some(encrypt_data_with_user_key(biography, &user_key)?) } else { None } } else { None };
|
||||
let encrypted_history: Option<String> = if let Some(ref history) = character.history { if !history.is_empty() { Some(encrypt_data_with_user_key(history, &user_key)?) } else { None } } else { None };
|
||||
let encrypted_speech_pattern: Option<String> = if let Some(ref speech_pattern) = character.speech_pattern { if !speech_pattern.is_empty() { Some(encrypt_data_with_user_key(speech_pattern, &user_key)?) } else { None } } else { None };
|
||||
let encrypted_catchphrase: Option<String> = if let Some(ref catchphrase) = character.catchphrase { if !catchphrase.is_empty() { Some(encrypt_data_with_user_key(catchphrase, &user_key)?) } else { None } } else { None };
|
||||
let encrypted_residence: Option<String> = if let Some(ref residence) = character.residence { if !residence.is_empty() { Some(encrypt_data_with_user_key(residence, &user_key)?) } else { None } } else { None };
|
||||
let encrypted_notes: Option<String> = if let Some(ref notes) = character.notes { if !notes.is_empty() { Some(encrypt_data_with_user_key(notes, &user_key)?) } else { None } } else { None };
|
||||
let encrypted_color: Option<String> = if let Some(ref color) = character.color { if !color.is_empty() { Some(encrypt_data_with_user_key(color, &user_key)?) } else { None } } else { None };
|
||||
|
||||
repo::update_character(
|
||||
conn, user_id, character_id, &encrypted_name,
|
||||
encrypted_last_name.as_deref(), encrypted_nickname.as_deref(), encrypted_age.as_deref(),
|
||||
encrypted_gender.as_deref(), encrypted_species.as_deref(), encrypted_nationality.as_deref(),
|
||||
encrypted_status.as_deref(), encrypted_title.as_deref(), encrypted_category.as_deref(),
|
||||
encrypted_image.as_deref(), encrypted_role.as_deref(), encrypted_biography.as_deref(),
|
||||
encrypted_history.as_deref(), encrypted_speech_pattern.as_deref(), encrypted_catchphrase.as_deref(),
|
||||
encrypted_residence.as_deref(), encrypted_notes.as_deref(), encrypted_color.as_deref(),
|
||||
last_update, lang,
|
||||
)
|
||||
}
|
||||
|
||||
/// Deletes a character from a series.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `deleted_at` - The timestamp of deletion
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the deletion was successful.
|
||||
pub fn delete_character(conn: &Connection, user_id: &str, character_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
|
||||
let exists: bool = repo::is_character_exist(conn, user_id, character_id, lang)?;
|
||||
if !exists {
|
||||
return Err(AppError::Internal(if lang == Lang::Fr { "Personnage non trouvé.".to_string() } else { "Character not found.".to_string() }));
|
||||
}
|
||||
let deleted: bool = repo::delete_character(conn, user_id, character_id, lang)?;
|
||||
if deleted {
|
||||
tombstone_repo::insert(conn, character_id, "series_characters", character_id, None, user_id, deleted_at, lang)?;
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
/// Adds a new attribute to a character.
|
||||
/// * `conn` - Database connection
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `attribute_type` - The attribute type
|
||||
/// * `name` - The attribute value
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the attribute ID.
|
||||
pub fn add_new_attribute(conn: &Connection, character_id: &str, user_id: &str, attribute_type: &str, name: &str, lang: Lang) -> AppResult<String> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let attribute_id: String = create_unique_id(None);
|
||||
let last_update: i64 = timestamp_in_seconds();
|
||||
let encrypted_type: String = encrypt_data_with_user_key(attribute_type, &user_key)?;
|
||||
let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?;
|
||||
|
||||
repo::insert_attribute(conn, &attribute_id, character_id, user_id, &encrypted_type, &encrypted_name, last_update, lang)?;
|
||||
|
||||
Ok(attribute_id)
|
||||
}
|
||||
|
||||
/// Deletes an attribute from a character.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `attribute_id` - The unique identifier of the attribute
|
||||
/// * `deleted_at` - The timestamp of deletion
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns true if the deletion was successful.
|
||||
pub fn delete_attribute(conn: &Connection, user_id: &str, attribute_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
|
||||
let deleted: bool = repo::delete_attribute(conn, user_id, attribute_id, lang)?;
|
||||
if deleted {
|
||||
tombstone_repo::insert(conn, attribute_id, "series_characters_attributes", attribute_id, None, user_id, deleted_at, lang)?;
|
||||
}
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
/// Gets all attributes for a character.
|
||||
/// * `conn` - Database connection
|
||||
/// * `user_id` - The unique identifier of the user
|
||||
/// * `character_id` - The unique identifier of the character
|
||||
/// * `lang` - The language for error messages
|
||||
/// Returns the character's attributes.
|
||||
pub fn get_character_attributes(conn: &Connection, user_id: &str, character_id: &str, lang: Lang) -> AppResult<CharacterAttributesResponse> {
|
||||
let user_key: String = get_user_encryption_key(user_id)?;
|
||||
let attributes_result: Vec<repo::SeriesCharacterAttributeResult> = repo::fetch_attributes(conn, character_id, user_id, lang)?;
|
||||
|
||||
let mut attributes: Vec<SeriesAttribute> = Vec::with_capacity(attributes_result.len());
|
||||
for attr in attributes_result {
|
||||
let decrypted_name: String = decrypt_data_with_user_key(&attr.attribute_value, &user_key)?;
|
||||
attributes.push(SeriesAttribute {
|
||||
id: attr.attr_id,
|
||||
name: decrypted_name,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(CharacterAttributesResponse { attributes })
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user