Add Books — read EPUB-imported books in Practice with tap-to-define
New "Books" row in the Practice tab opens a library of bundled bilingual books. Each chapter renders Spanish paragraph-by-paragraph; tap any word for a definition sheet (DictionaryService with on-device AI fallback), or toggle the toolbar button to swap to the pre-computed English translation inline. Local-only Book + BookChapter SwiftData models added to the local container schema (reset version bumped to 5). DataLoader.seedBooks walks the bundle for `book_*.json` resources, so future books drop in without touching app code — just bundle a new JSON and bump bookDataVersion. First book: Olly Richards' "Spanish Short Stories For Beginners Vol 2" — 13 chapters, 2,646 paragraphs, bilingual. Scripts/books/ is the repeatable pipeline for future EPUBs: extract_epub.py → translate_chapters.py (per-chapter resumable jobs) → bundle_book.py. Translation is done by parallel Claude Code subagents reading per-job input files and writing output files — no API key required, matching the pattern used for the textbook vocab vision pass. See Scripts/books/README.md for the full how-to. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
/// A long-form bilingual book bundled with the app. Chapter content lives in
|
||||
/// `BookChapter` rows; this model carries the per-book metadata.
|
||||
@Model
|
||||
public final class Book {
|
||||
@Attribute(.unique) public var id: String = "" // matches `slug`
|
||||
public var slug: String = ""
|
||||
public var title: String = ""
|
||||
public var author: String = ""
|
||||
public var language: String = ""
|
||||
public var chapterCount: Int = 0
|
||||
public var accentColorHex: String = ""
|
||||
|
||||
public init(
|
||||
slug: String,
|
||||
title: String,
|
||||
author: String,
|
||||
language: String,
|
||||
chapterCount: Int,
|
||||
accentColorHex: String
|
||||
) {
|
||||
self.id = slug
|
||||
self.slug = slug
|
||||
self.title = title
|
||||
self.author = author
|
||||
self.language = language
|
||||
self.chapterCount = chapterCount
|
||||
self.accentColorHex = accentColorHex
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import Foundation
|
||||
import SwiftData
|
||||
|
||||
/// One chapter of a `Book`. Spanish + English paragraphs are stored as JSON-
|
||||
/// encoded `[String]` so SwiftData doesn't have to manage variable-length
|
||||
/// arrays directly.
|
||||
@Model
|
||||
public final class BookChapter {
|
||||
@Attribute(.unique) public var id: String = "" // "<bookSlug>-ch<number>"
|
||||
public var bookSlug: String = ""
|
||||
public var number: Int = 0
|
||||
public var title: String = ""
|
||||
public var paragraphsESJSON: Data = Data()
|
||||
public var paragraphsENJSON: Data = Data()
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
bookSlug: String,
|
||||
number: Int,
|
||||
title: String,
|
||||
paragraphsESJSON: Data,
|
||||
paragraphsENJSON: Data
|
||||
) {
|
||||
self.id = id
|
||||
self.bookSlug = bookSlug
|
||||
self.number = number
|
||||
self.title = title
|
||||
self.paragraphsESJSON = paragraphsESJSON
|
||||
self.paragraphsENJSON = paragraphsENJSON
|
||||
}
|
||||
|
||||
public func paragraphsES() -> [String] {
|
||||
(try? JSONDecoder().decode([String].self, from: paragraphsESJSON)) ?? []
|
||||
}
|
||||
|
||||
public func paragraphsEN() -> [String] {
|
||||
(try? JSONDecoder().decode([String].self, from: paragraphsENJSON)) ?? []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user