iOS: fix audio polling, AVPlayer stability, and misc UX issues
Some checks failed
CI / UI / Build (pull_request) Failing after 5s
CI / Scraper / Test (pull_request) Failing after 14s
CI / Scraper / Lint (pull_request) Failing after 15s
CI / Scraper / Build (pull_request) Has been skipped
iOS CI / Build (push) Has been cancelled
iOS CI / Test (push) Has been cancelled
iOS CI / Build (pull_request) Failing after 2s
iOS CI / Test (pull_request) Has been skipped
Some checks failed
CI / UI / Build (pull_request) Failing after 5s
CI / Scraper / Test (pull_request) Failing after 14s
CI / Scraper / Lint (pull_request) Failing after 15s
CI / Scraper / Build (pull_request) Has been skipped
iOS CI / Build (push) Has been cancelled
iOS CI / Test (push) Has been cancelled
iOS CI / Build (pull_request) Failing after 2s
iOS CI / Test (pull_request) Has been skipped
- AudioPlayerService: replace loading/generating states with a single
.generating state; presign fast path before triggering TTS; fix URL
resolution for relative paths; cache cover artwork to avoid re-downloads;
fix duration KVO race (durationObserver); use toleranceBefore/After:zero
for accurate seeking; prefetch next chapter unconditionally (not just when
autoNext is on); handle auto-next internally on playback finish
- APIClient: fix URL construction (appendingPathComponent encodes slashes);
add verbose debug logging for all requests/responses/decoding errors;
fix sessions() to unwrap {sessions:[]} envelope; fix BrowseResponse
CodingKey hasNext → camelCase
- AudioGenerateResponse: update to synchronous {url, filename} shape
- Models: remove redundant AudioStatus enum; remove CodingKeys from
UserSettings (server now sends camelCase)
- Views: fix alert bindings (.constant → proper two-way Binding); add
error+retry UI to BrowseView; add pull-to-refresh to browse list;
fix ChapterReaderView to show error state and use dynamic WKWebView
height; fix HomeView HStack alignment; only treat audio as current
if the player isActive; suppress CancellationError from error UI
This commit is contained in:
@@ -18,19 +18,46 @@ struct ChapterReaderView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let _ = print("[ChapterReaderView] body eval — slug=\(slug) ch=\(chapterNumber) isLoading=\(vm.isLoading) hasContent=\(vm.content != nil) error=\(vm.error ?? "nil")")
|
||||
Group {
|
||||
if vm.isLoading {
|
||||
let _ = print("[ChapterReaderView] branch: LOADING")
|
||||
ProgressView().frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else if let content = vm.content {
|
||||
let _ = print("[ChapterReaderView] branch: CONTENT html=\(content.html.count)chars title='\(content.chapter.title)'")
|
||||
readerContent(content)
|
||||
} else if let errMsg = vm.error {
|
||||
let _ = print("[ChapterReaderView] branch: ERROR '\(errMsg)'")
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.font(.largeTitle)
|
||||
.foregroundStyle(.orange)
|
||||
Text(errMsg)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.horizontal)
|
||||
Button("Retry") { Task { await vm.load() } }
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(.amber)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
let _ = print("[ChapterReaderView] branch: BLANK (no loading, no content, no error)")
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
.navigationTitle(vm.content.map { "Ch.\($0.chapter.number)" } ?? "")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { audioToolbarButton }
|
||||
.task { await vm.load() }
|
||||
.task {
|
||||
print("[ChapterReaderView] .task fired — calling vm.load()")
|
||||
await vm.load()
|
||||
print("[ChapterReaderView] .task completed")
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .audioDidFinishChapter)) { note in
|
||||
if let next = note.userInfo?["next"] as? Int {
|
||||
guard let next = note.userInfo?["next"] as? Int else { return }
|
||||
let shouldAutoNavigate = note.userInfo?["autoNext"] as? Bool ?? false
|
||||
if shouldAutoNavigate {
|
||||
vm.navigateTo = next
|
||||
}
|
||||
}
|
||||
@@ -42,15 +69,15 @@ struct ChapterReaderView: View {
|
||||
ChapterReaderView(slug: slug, chapterNumber: next)
|
||||
}
|
||||
}
|
||||
.alert("Error", isPresented: .constant(vm.error != nil)) {
|
||||
Button("OK") { vm.error = nil }
|
||||
} message: { Text(vm.error ?? "") }
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
|
||||
@State private var webHeight: CGFloat = 800
|
||||
|
||||
@ViewBuilder
|
||||
private func readerContent(_ content: ChapterResponse) -> some View {
|
||||
let _ = print("[ChapterReaderView] readerContent — html=\(content.html.count)chars webHeight=\(webHeight)")
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
// Header
|
||||
@@ -68,7 +95,8 @@ struct ChapterReaderView: View {
|
||||
Divider()
|
||||
|
||||
// Chapter body
|
||||
HTMLContentView(html: content.html)
|
||||
HTMLContentView(html: content.html, height: $webHeight)
|
||||
.frame(height: webHeight)
|
||||
.padding(.horizontal)
|
||||
|
||||
Divider()
|
||||
@@ -119,30 +147,62 @@ struct ChapterReaderView: View {
|
||||
|
||||
struct HTMLContentView: UIViewRepresentable {
|
||||
let html: String
|
||||
@Binding var height: CGFloat
|
||||
|
||||
func makeCoordinator() -> Coordinator { Coordinator(self) }
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
let wv = WKWebView()
|
||||
wv.scrollView.isScrollEnabled = false
|
||||
wv.isOpaque = false
|
||||
wv.backgroundColor = .clear
|
||||
wv.scrollView.backgroundColor = .clear
|
||||
wv.navigationDelegate = context.coordinator
|
||||
return wv
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: WKWebView, context: Context) {
|
||||
let isDark = UITraitCollection.current.userInterfaceStyle == .dark
|
||||
let textColor = isDark ? "#e5e5e5" : "#1a1a1a"
|
||||
let css = """
|
||||
body {
|
||||
font-family: -apple-system, Georgia, serif;
|
||||
font-size: 17px;
|
||||
line-height: 1.7;
|
||||
color: \(UITraitCollection.current.userInterfaceStyle == .dark ? "#e5e5e5" : "#1a1a1a");
|
||||
color: \(textColor);
|
||||
background: transparent;
|
||||
margin: 0; padding: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
p { margin: 0 0 1em 0; }
|
||||
"""
|
||||
let wrapped = "<html><head><style>\(css)</style><meta name='viewport' content='width=device-width, initial-scale=1'></head><body>\(html)</body></html>"
|
||||
print("[HTMLContentView] updateUIView — html=\(html.count)chars loading into WKWebView")
|
||||
uiView.loadHTMLString(wrapped, baseURL: nil)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, WKNavigationDelegate {
|
||||
var parent: HTMLContentView
|
||||
init(_ parent: HTMLContentView) { self.parent = parent }
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
print("[HTMLContentView] didFinish — evaluating scrollHeight")
|
||||
webView.evaluateJavaScript("document.body.scrollHeight") { result, error in
|
||||
DispatchQueue.main.async {
|
||||
print("[HTMLContentView] scrollHeight result=\(String(describing: result)) error=\(String(describing: error))")
|
||||
if let h = result as? CGFloat, h > 0 {
|
||||
print("[HTMLContentView] height set to CGFloat \(h)")
|
||||
self.parent.height = h
|
||||
} else if let h = result as? Double, h > 0 {
|
||||
print("[HTMLContentView] height set to Double \(h)")
|
||||
self.parent.height = CGFloat(h)
|
||||
} else {
|
||||
print("[HTMLContentView] ⚠️ could not read height — keeping \(self.parent.height)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Reverse label style (icon on right)
|
||||
|
||||
Reference in New Issue
Block a user