diff --git a/BACKEND/app/crud.py b/BACKEND/app/crud.py index 00b589e..db95d54 100644 --- a/BACKEND/app/crud.py +++ b/BACKEND/app/crud.py @@ -2084,7 +2084,7 @@ def fetch_book_metadata(book: str, book_metadata: dict) -> dict: "long_title": book_metadata[book]["long"]["en"], } -def generate_usfm_content(book, book_info, chapter_map, versification_data, db): +def generate_usfm_content(book, book_info, chapter_map, versification_data, db, single_chapter: int = None): """ Generate USFM formatted content with book metadata and verses. """ @@ -2099,6 +2099,28 @@ def generate_usfm_content(book, book_info, chapter_map, versification_data, db): raise HTTPException( status_code=404, detail=f"Versification data not found for book '{book}'." ) + + # --- If single_chapter specified, process only that one --- + if single_chapter: + if single_chapter not in chapter_map: + raise HTTPException( + status_code=404, + detail=f"Chapter {single_chapter} not found in book '{book}'.", + ) + + num_verses = int(max_verses[single_chapter - 1]) + usfm_text += f"\\c {single_chapter}\n\\p\n" + + chapter = chapter_map[single_chapter] + verses = db.query(Verse).filter(Verse.chapter_id == chapter.chapter_id).all() + verse_map = {verse.verse: verse.text for verse in verses} + + for verse_number in range(1, num_verses + 1): + usfm_text += f"\\v {verse_number} {verse_map.get(verse_number, '...')}\n" + + return usfm_text + + # --- Process all chapters --- for chapter_number, num_verses in enumerate(max_verses, start=1): try: num_verses = int(num_verses) diff --git a/BACKEND/app/router.py b/BACKEND/app/router.py index 38137cb..f7cbbc5 100644 --- a/BACKEND/app/router.py +++ b/BACKEND/app/router.py @@ -972,10 +972,24 @@ async def generate_usfm( # Validate if the book exists in metadata # Fetch chapters and verses - chapters = db.query(Chapter).filter(Chapter.book_id == book_id).all() - chapter_map = {chapter.chapter: chapter for chapter in chapters} + if chapter is not None: + chapters = ( + db.query(Chapter) + .filter(Chapter.book_id == book_id, Chapter.chapter == chapter) + .all() + ) + if not chapters: + raise HTTPException( + status_code=404, + detail=f"Chapter {chapter} not found in book {book}" + ) + else: + chapters = db.query(Chapter).filter(Chapter.book_id == book_id).all() + + # Prepare chapter map for downstream USFM generation + chapter_map = {ch.chapter: ch for ch in chapters} # Generate USFM content - usfm_text = crud.generate_usfm_content(book, book_info, chapter_map, versification_data, db) + usfm_text = crud.generate_usfm_content(book, book_info, chapter_map, versification_data, db, single_chapter=chapter) return crud.save_and_return_usfm_file(project, book, usfm_text) diff --git a/UI/src/components/ChapterModal.tsx b/UI/src/components/ChapterModal.tsx index cfbb049..a5d8ffc 100644 --- a/UI/src/components/ChapterModal.tsx +++ b/UI/src/components/ChapterModal.tsx @@ -20,6 +20,9 @@ import { clearStoredVersesForChapter, hasPendingChanges, } from "@/utils/chapterStorage"; +import useAuthStore from "@/store/useAuthStore"; + +const BASE_URL = import.meta.env.VITE_BASE_URL; interface Verse { verse_id: number; @@ -389,6 +392,54 @@ const ChapterModal: React.FC = ({ return `${remaining} verse(s) left`; }; + const handleDownloadChapter = async ( + projectId: number, + bookName: string, + chapter: number + ) => { + try { + const response = await fetch( + `${BASE_URL}/generate-usfm/?project_id=${projectId}&book=${bookName}&chapter=${chapter}`, + { + method: "GET", + headers: { + Authorization: `Bearer ${useAuthStore.getState().token}`, + "Content-Type": "application/json", + }, + } + ); + if (!response.ok) { + const responseData = await response.json(); + throw new Error(responseData.detail || "Failed to generate USFM"); + } + const contentDisposition = response.headers.get("Content-Disposition"); + let fileName = `${bookName}.usfm`; + if (contentDisposition) { + const match = contentDisposition.match(/filename="([^"]+)"/); + if (match && match[1]) { + fileName = match[1]; + } + } + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = fileName; + link.click(); + URL.revokeObjectURL(url); + toast({ + variant: "success", + title: "File downloaded successfully!", + }); + } catch (error) { + toast({ + variant: "destructive", + title: + error instanceof Error ? error.message : "Error while downloading", + }); + } + }; + const handleCloseModal = () => { // Save changes if needed // if ( @@ -589,6 +640,12 @@ const ChapterModal: React.FC = ({ > {approved ? "Unapprove" : "Approve"} +
toast({