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

- 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:
Admin
2026-03-07 20:13:40 +05:00
parent 89f0dfb113
commit 460e7553bf
15 changed files with 354 additions and 142 deletions

View File

@@ -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)