iOS: codebase cleanup — remove dead code, unify design, deduplicate patterns
Some checks failed
CI / UI / Build (pull_request) Failing after 7s
CI / Scraper / Test (pull_request) Failing after 10s
CI / Scraper / Lint (pull_request) Failing after 10s
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 7s
CI / Scraper / Test (pull_request) Failing after 10s
CI / Scraper / Lint (pull_request) Failing after 10s
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
- Remove swift-markdown-ui dependency (project.yml, pbxproj, Package.resolved) - Remove all debug print() statements (APIClient, BrowseViewModel, ChapterReaderViewModel, ChapterReaderView) - Remove dead UI stubs: isFavorited/isLiked state, heart/star buttons, empty Share/Add to Library menu items in FullPlayerView - Remove unused audioToolbarButton toolbar builder in ChapterReaderView - Remove unused model types: NovelListing, BookDetailData, ChapterContent - Remove amberLight color (never referenced) - Remove mini player interactive seek (horizontal drag/tap to seek) — progress bar is now display-only - Fix AccentColor asset to amber #f59e0b (was orange-pink mismatch) - Fix Discover tab icon: globe → globe.americas.fill - Fix speed slider max: 3.0 → 2.0 in ProfileView - Fix yearText: use static DateFormatter instead of allocating on every access - Fix ChangePasswordView.save(): DispatchQueue.main.asyncAfter → Task.sleep - Deduplicate navigationDestination blocks via .appNavigationDestination() View extension - Deduplicate .alert(Error) pattern via .errorAlert() View extension
This commit is contained in:
@@ -7,6 +7,9 @@ struct ChapterReaderView: View {
|
||||
let slug: String
|
||||
let chapterNumber: Int
|
||||
|
||||
/// Tracks the currently displayed chapter — updated in-place by skip/auto-next
|
||||
/// so we never accumulate stale listeners on the navigation stack.
|
||||
@State private var currentChapter: Int
|
||||
@StateObject private var vm: ChapterReaderViewModel
|
||||
@EnvironmentObject var audioPlayer: AudioPlayerService
|
||||
@EnvironmentObject var authStore: AuthStore
|
||||
@@ -14,20 +17,17 @@ struct ChapterReaderView: View {
|
||||
init(slug: String, chapterNumber: Int) {
|
||||
self.slug = slug
|
||||
self.chapterNumber = chapterNumber
|
||||
_currentChapter = State(initialValue: chapterNumber)
|
||||
_vm = StateObject(wrappedValue: ChapterReaderViewModel(slug: slug, chapter: chapterNumber))
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -42,7 +42,6 @@ struct ChapterReaderView: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
let _ = print("[ChapterReaderView] branch: BLANK (no loading, no content, no error)")
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
@@ -54,43 +53,42 @@ struct ChapterReaderView: View {
|
||||
floatingAudioButton
|
||||
}
|
||||
}
|
||||
.task {
|
||||
print("[ChapterReaderView] .task fired — calling vm.load()")
|
||||
.task(id: currentChapter) {
|
||||
await vm.load()
|
||||
print("[ChapterReaderView] .task completed")
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .audioDidFinishChapter)) { note in
|
||||
guard let next = note.userInfo?["next"] as? Int else { return }
|
||||
let shouldAutoNavigate = note.userInfo?["autoNext"] as? Bool ?? false
|
||||
if shouldAutoNavigate {
|
||||
vm.navigateTo = next
|
||||
}
|
||||
// Only handle if this is the top-most (currently active) chapter view
|
||||
guard shouldAutoNavigate, currentChapter == audioPlayer.chapter else { return }
|
||||
navigateToChapter(next)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .skipToNextChapter)) { note in
|
||||
guard let next = note.userInfo?["next"] as? Int else { return }
|
||||
vm.navigateTo = next
|
||||
// Only the view whose chapter matches the currently playing chapter should handle this
|
||||
guard currentChapter == audioPlayer.chapter else { return }
|
||||
navigateToChapter(next)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .skipToPrevChapter)) { note in
|
||||
guard let prev = note.userInfo?["prev"] as? Int else { return }
|
||||
vm.navigateTo = prev
|
||||
}
|
||||
.navigationDestination(isPresented: Binding(
|
||||
get: { vm.navigateTo != nil },
|
||||
set: { if !$0 { vm.navigateTo = nil } }
|
||||
)) {
|
||||
if let next = vm.navigateTo {
|
||||
ChapterReaderView(slug: slug, chapterNumber: next)
|
||||
}
|
||||
guard currentChapter == audioPlayer.chapter else { return }
|
||||
navigateToChapter(prev)
|
||||
}
|
||||
}
|
||||
|
||||
/// Navigate to a chapter in-place: reloads content without pushing to the navigation stack.
|
||||
/// Back button always returns to BookDetailView regardless of how many chapters were visited.
|
||||
private func navigateToChapter(_ chapter: Int) {
|
||||
vm.switchChapter(to: chapter)
|
||||
currentChapter = chapter
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -114,17 +112,21 @@ struct ChapterReaderView: View {
|
||||
|
||||
Divider()
|
||||
|
||||
// Prev / Next navigation
|
||||
// Prev / Next navigation — in-place swap so back button always returns to book
|
||||
HStack(spacing: 12) {
|
||||
if let prev = content.prev {
|
||||
NavigationLink(value: NavDestination.chapter(slug, prev)) {
|
||||
Button {
|
||||
navigateToChapter(prev)
|
||||
} label: {
|
||||
Label("Ch.\(prev)", systemImage: "chevron.left")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
}
|
||||
if let next = content.next {
|
||||
NavigationLink(value: NavDestination.chapter(slug, next)) {
|
||||
Button {
|
||||
navigateToChapter(next)
|
||||
} label: {
|
||||
Label("Ch.\(next)", systemImage: "chevron.right")
|
||||
.labelStyle(ReverseLabelStyle())
|
||||
.frame(maxWidth: .infinity)
|
||||
@@ -171,23 +173,6 @@ struct ChapterReaderView: View {
|
||||
.padding(.trailing, 20)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
|
||||
// MARK: - Audio toolbar button (kept for when player is active)
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var audioToolbarButton: some ToolbarContent {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button {
|
||||
vm.toggleAudio(audioPlayer: audioPlayer, settings: authStore.settings)
|
||||
} label: {
|
||||
Image(systemName: audioPlayer.isActive &&
|
||||
audioPlayer.slug == slug &&
|
||||
audioPlayer.chapter == chapterNumber
|
||||
? "speaker.wave.2.fill" : "speaker.wave.2")
|
||||
.foregroundStyle(.amber)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HTML content renderer using WKWebView
|
||||
@@ -224,7 +209,6 @@ struct HTMLContentView: UIViewRepresentable {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -233,18 +217,12 @@ struct HTMLContentView: UIViewRepresentable {
|
||||
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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user