131 lines
3.9 KiB
Swift
131 lines
3.9 KiB
Swift
import SwiftUI
|
|
import SwiftData
|
|
import Storage
|
|
import BookParser
|
|
import AudioEngine
|
|
import VorleserKit
|
|
|
|
struct MacReaderView: View {
|
|
let storedBook: StoredBook
|
|
@State private var viewModel = ReaderViewModel()
|
|
@State private var scrollToChapterOffset: CharacterOffset?
|
|
@State private var fullAttributedText: NSAttributedString?
|
|
@State private var readingMode: String = "scroll"
|
|
@Environment(\.modelContext) private var modelContext
|
|
|
|
private var bookStore: BookStore {
|
|
BookStore(
|
|
modelContainer: modelContext.container,
|
|
documentsDirectory: FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
|
)
|
|
}
|
|
|
|
var body: some View {
|
|
VStack {
|
|
if let error = viewModel.error {
|
|
ContentUnavailableView("Error", systemImage: "exclamationmark.triangle", description: Text(error))
|
|
} else if let book = viewModel.book, let attrText = fullAttributedText {
|
|
toolbar(book: book)
|
|
readingContent(attributedText: attrText)
|
|
MacPlaybackControls(engine: viewModel.engine)
|
|
} else {
|
|
ProgressView("Loading\u{2026}")
|
|
}
|
|
}
|
|
.navigationTitle(storedBook.title)
|
|
.task { await loadBook() }
|
|
.onDisappear {
|
|
viewModel.engine.stop()
|
|
storedBook.readingMode = readingMode
|
|
try? bookStore.updatePosition(storedBook, position: viewModel.engine.currentPosition)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func toolbar(book: Book) -> some View {
|
|
HStack {
|
|
if book.chapters.count > 1 {
|
|
Picker("Chapter", selection: Binding(
|
|
get: { viewModel.selectedChapterIndex },
|
|
set: { newIndex in
|
|
viewModel.selectedChapterIndex = newIndex
|
|
scrollToChapterOffset = viewModel.chapterOffset(for: newIndex)
|
|
}
|
|
)) {
|
|
ForEach(book.chapters, id: \.index) { chapter in
|
|
Text(chapter.title).tag(chapter.index)
|
|
}
|
|
}
|
|
.pickerStyle(.menu)
|
|
}
|
|
Picker("Mode", selection: $readingMode) {
|
|
Label("Scroll", systemImage: "scroll").tag("scroll")
|
|
Label("Book", systemImage: "book").tag("book")
|
|
}
|
|
.pickerStyle(.segmented)
|
|
.frame(maxWidth: 200)
|
|
}
|
|
.padding(.horizontal)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func readingContent(attributedText: NSAttributedString) -> some View {
|
|
let highlightRange = viewModel.currentSentenceRange()
|
|
|
|
if readingMode == "book" {
|
|
PagedBookView(
|
|
attributedText: attributedText,
|
|
highlightRange: highlightRange,
|
|
onClickCharacter: { offset in
|
|
Task { try await viewModel.startPlayback(from: offset) }
|
|
},
|
|
scrollToOffset: scrollToChapterOffset
|
|
)
|
|
} else {
|
|
BookTextView(
|
|
attributedText: attributedText,
|
|
highlightRange: highlightRange,
|
|
onClickCharacter: { offset in
|
|
Task { try await viewModel.startPlayback(from: offset) }
|
|
},
|
|
scrollToOffset: scrollToChapterOffset
|
|
)
|
|
}
|
|
}
|
|
|
|
private func loadBook() async {
|
|
readingMode = storedBook.readingMode ?? "scroll"
|
|
let fileURL = bookStore.fileURL(for: storedBook)
|
|
let modelURL = Bundle.main.url(forResource: "kokoro-v1_0", withExtension: "safetensors")
|
|
let voicesURL = Bundle.main.url(forResource: "voices", withExtension: "npz")
|
|
await viewModel.loadBook(
|
|
fileURL: fileURL,
|
|
fileExists: bookStore.fileExists(for: storedBook),
|
|
lastPosition: storedBook.lastPosition,
|
|
modelURL: modelURL,
|
|
voicesURL: voicesURL
|
|
)
|
|
if let book = viewModel.book {
|
|
fullAttributedText = Self.buildFullAttributedText(from: book)
|
|
}
|
|
}
|
|
|
|
private static func buildFullAttributedText(from book: Book) -> NSAttributedString {
|
|
let result = NSMutableAttributedString()
|
|
for (i, chapter) in book.chapters.enumerated() {
|
|
if i > 0 {
|
|
let chapterAttr = NSMutableAttributedString(attributedString: chapter.attributedText)
|
|
if chapterAttr.length > 0 {
|
|
let paragraphStyle = NSMutableParagraphStyle()
|
|
paragraphStyle.paragraphSpacingBefore = 32
|
|
chapterAttr.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: 1))
|
|
}
|
|
result.append(chapterAttr)
|
|
} else {
|
|
result.append(chapter.attributedText)
|
|
}
|
|
}
|
|
return NSAttributedString(attributedString: result)
|
|
}
|
|
}
|