diff --git a/Sora/Utlis & Misc/JSLoader/JSController-Novel.swift b/Sora/Utlis & Misc/JSLoader/JSController-Novel.swift index 01aa397..00d3fa2 100644 --- a/Sora/Utlis & Misc/JSLoader/JSController-Novel.swift +++ b/Sora/Utlis & Misc/JSLoader/JSController-Novel.swift @@ -267,7 +267,7 @@ extension JSController { Logger.shared.log("Attempting direct fetch from: \(url.absoluteString)", type: "Debug") - let task = URLSession.shared.dataTask(with: request) { data, response, error in + let task = URLSession.shared.dataTask(with: request) { [self] data, response, error in if let error = error { DispatchQueue.main.async { Logger.shared.log("Direct fetch error: \(error.localizedDescription)", type: "Error") @@ -300,25 +300,50 @@ extension JSController { let startIndex = contentRange.lowerBound let endIndex = endRange.upperBound content = String(htmlString[startIndex.. tag", type: "Debug") } else if let contentRange = htmlString.range(of: "
", options: .caseInsensitive, range: contentRange.upperBound..", options: .caseInsensitive, range: contentRange.upperBound..", options: .caseInsensitive, range: contentRange.upperBound..", options: .caseInsensitive, range: contentRange.upperBound..", options: .caseInsensitive) { + let startIndex = contentRange.lowerBound + let endIndex = endRange.upperBound + content = String(htmlString[startIndex.. tag", type: "Debug") } else if let bodyRange = htmlString.range(of: "", options: .caseInsensitive) { let startIndex = bodyRange.lowerBound let endIndex = endBodyRange.upperBound content = String(htmlString[startIndex.. tag", type: "Debug") } else { content = htmlString + Logger.shared.log("Using full HTML content", type: "Debug") } + content = cleanHTMLContent(content) + DispatchQueue.main.async { Logger.shared.log("Direct fetch successful, content length: \(content.count)", type: "Debug") completion(.success(content)) @@ -327,4 +352,55 @@ extension JSController { task.resume() } + + private func cleanHTMLContent(_ content: String) -> String { + var cleaned = content + + cleaned = cleaned.replacingOccurrences( + of: "]*>.*?", + with: "", + options: [.regularExpression, .caseInsensitive] + ) + + cleaned = cleaned.replacingOccurrences( + of: "]*>.*?", + with: "", + options: [.regularExpression, .caseInsensitive] + ) + + cleaned = cleaned.replacingOccurrences( + of: "]*>.*?", + with: "", + options: [.regularExpression, .caseInsensitive] + ) + + cleaned = cleaned.replacingOccurrences( + of: "]*>.*?", + with: "", + options: [.regularExpression, .caseInsensitive] + ) + + cleaned = cleaned.replacingOccurrences( + of: "]*>.*?", + with: "", + options: [.regularExpression, .caseInsensitive] + ) + + let unwantedClasses = ["advertisement", "ad", "ads", "sidebar", "menu", "navigation", "nav", "header", "footer", "comments", "comment"] + for className in unwantedClasses { + cleaned = cleaned.replacingOccurrences( + of: "]*class=\"[^\"]*\(className)[^\"]*\"[^>]*>.*?
", + with: "", + options: [.regularExpression, .caseInsensitive] + ) + } + + cleaned = cleaned.replacingOccurrences( + of: "\\s+", + with: " ", + options: .regularExpression + ) + + return cleaned.trimmingCharacters(in: .whitespacesAndNewlines) + } } diff --git a/Sora/Views/MediaInfoView/MediaInfoView.swift b/Sora/Views/MediaInfoView/MediaInfoView.swift index be9f8ed..6313b0a 100644 --- a/Sora/Views/MediaInfoView/MediaInfoView.swift +++ b/Sora/Views/MediaInfoView/MediaInfoView.swift @@ -1221,6 +1221,13 @@ struct MediaInfoView: View { let maxIndex = max(0, groupedEpisodes().count - 1) selectedSeason = min(savedSeason, maxIndex) } + + if let savedChapterStart = UserDefaults.standard.object(forKey: selectedChapterRangeKey) as? Int, + let savedChapterRange = generateChapterRanges().first(where: { $0.lowerBound == savedChapterStart }) { + selectedChapterRange = savedChapterRange + } else { + selectedChapterRange = generateChapterRanges().first ?? 0.. [Range] { @@ -1267,6 +1274,41 @@ struct MediaInfoView: View { } private func playFirstUnwatchedEpisode() { + if module.metadata.novel ?? false { + guard !chapters.isEmpty else { return } + + var firstUnreadChapter: [String: Any]? = nil + for chapter in chapters { + if let href = chapter["href"] as? String { + let progress = UserDefaults.standard.double(forKey: "readingProgress_\(href)") + if progress < 0.95 { + firstUnreadChapter = chapter + break + } + } + } + + let chapterToRead = firstUnreadChapter ?? chapters[0] + + if let href = chapterToRead["href"] as? String, + let title = chapterToRead["title"] as? String, + let number = chapterToRead["number"] as? Int { + + UserDefaults.standard.set(true, forKey: "navigatingToReaderView") + ChapterNavigator.shared.currentChapter = ( + moduleId: module.id, + href: href, + title: title, + chapters: chapters, + mediaTitle: self.title, + chapterNumber: number + ) + + Logger.shared.log("Navigating to chapter: \(title)", type: "Debug") + } + return + } + let indices = finishedAndUnfinishedIndices() let finished = indices.finished let unfinished = indices.unfinished @@ -1415,10 +1457,17 @@ struct MediaInfoView: View { self.jsController.loadScript(jsContent) let completion: (Any?, [EpisodeLink]) -> Void = { items, episodes in - if self.module.metadata.novel ?? true { + if self.module.metadata.novel ?? false { + self.processItemsResponse(items) + self.jsController.extractChapters(moduleId: self.module.id, href: self.href) { chapters in DispatchQueue.main.async { - self.handleFetchDetailsResponse(items: chapters, episodes: episodes) + self.chapters = chapters + Logger.shared.log("fetchDetails: (novel) chapters count = \(self.chapters.count)", type: "Debug") + self.restoreSelectionState() + + self.isLoading = false + self.isRefetching = false } } } else { @@ -1442,22 +1491,11 @@ struct MediaInfoView: View { private func handleFetchDetailsResponse(items: Any?, episodes: [EpisodeLink]) { Logger.shared.log("fetchDetails: items = \(String(describing: items))", type: "Debug") Logger.shared.log("fetchDetails: episodes = \(episodes)", type: "Debug") - processItemsResponse(items) - if module.metadata.novel ?? false { - if let chaptersData = items as? [[String: Any]] { - chapters = chaptersData - Logger.shared.log("fetchDetails: (novel) chapters count = \(chapters.count)", type: "Debug") - } else { - Logger.shared.log("fetchDetails: (novel) no chapters found in response", type: "Warning") - chapters = [] - } - } else { - Logger.shared.log("fetchDetails: (episodes) episodes count = \(episodes.count)", type: "Debug") - episodeLinks = episodes - restoreSelectionState() - } + Logger.shared.log("fetchDetails: (episodes) episodes count = \(episodes.count)", type: "Debug") + episodeLinks = episodes + restoreSelectionState() isLoading = false isRefetching = false diff --git a/Sora/Views/ReaderView/ReaderView.swift b/Sora/Views/ReaderView/ReaderView.swift index 46e397e..e233f42 100644 --- a/Sora/Views/ReaderView/ReaderView.swift +++ b/Sora/Views/ReaderView/ReaderView.swift @@ -52,10 +52,8 @@ struct ReaderView: View { @State private var readingProgress: Double = 0.0 @State private var lastProgressUpdate: Date = Date() @Environment(\.dismiss) private var dismiss - - @StateObject private var navigator = ChapterNavigator.shared - // Status bar control + @StateObject private var navigator = ChapterNavigator.shared @State private var statusBarHidden = false private let fontOptions = [ @@ -125,7 +123,7 @@ struct ReaderView: View { } } - + var body: some View { ZStack(alignment: .bottom) { @@ -161,7 +159,7 @@ struct ReaderView: View { } } .frame(maxWidth: .infinity, maxHeight: .infinity) - + HTMLView( htmlContent: htmlContent, fontSize: fontSize, @@ -208,7 +206,7 @@ struct ReaderView: View { .zIndex(1) if isHeaderVisible { - footerView + footerView .transition(.move(edge: .bottom)) .zIndex(2) } @@ -268,7 +266,7 @@ struct ReaderView: View { mediaTitle: next.mediaTitle, chapterNumber: next.chapterNumber ) - + let hostingController = UIHostingController(rootView: nextReader) hostingController.modalPresentationStyle = .fullScreen @@ -280,8 +278,8 @@ struct ReaderView: View { } else { if !htmlContent.isEmpty { let validHtmlContent = (!htmlContent.isEmpty && - !htmlContent.contains("undefined") && - htmlContent.count > 50) ? htmlContent : nil + !htmlContent.contains("undefined") && + htmlContent.count > 50) ? htmlContent : nil if validHtmlContent == nil { Logger.shared.log("Not caching HTML content on disappear as it appears invalid", type: "Warning") @@ -330,6 +328,9 @@ struct ReaderView: View { self.setStatusBarHidden(true) } } + } else { + Logger.shared.log("No valid cached content found, fetching new content for \(self.chapterHref)", type: "Debug") + fetchContentWithRetries(attempts: 0, maxAttempts: 3) } } catch { self.error = error @@ -378,14 +379,16 @@ struct ReaderView: View { return } - self.htmlContent = content - self.isLoading = false - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - withAnimation(.easeInOut(duration: 0.3)) { - self.isHeaderVisible = false - self.statusBarHidden = true - self.setStatusBarHidden(true) + DispatchQueue.main.async { + self.htmlContent = content + self.isLoading = false + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + withAnimation(.easeInOut(duration: 0.3)) { + self.isHeaderVisible = false + self.statusBarHidden = true + self.setStatusBarHidden(true) + } } } @@ -448,7 +451,7 @@ struct ReaderView: View { .padding(.trailing, 100) Spacer() - + Color.clear .frame(width: 44, height: 44) .padding(.trailing) @@ -463,7 +466,7 @@ struct ReaderView: View { isSettingsExpanded = false } } - + HStack { Spacer() Button(action: { @@ -935,8 +938,8 @@ struct ReaderView: View { Logger.shared.log("Saving continue reading item: title=\(novelTitle), chapter=\(chapterTitle), number=\(currentChapterNumber), href=\(chapterHref), progress=\(progress), imageUrl=\(imageUrl)", type: "Debug") let validHtmlContent = (!htmlContent.isEmpty && - !htmlContent.contains("undefined") && - htmlContent.count > 50) ? htmlContent : nil + !htmlContent.contains("undefined") && + htmlContent.count > 50) ? htmlContent : nil if validHtmlContent == nil && !htmlContent.isEmpty { Logger.shared.log("Not caching HTML content as it appears invalid", type: "Warning") @@ -1008,8 +1011,8 @@ struct ReaderView: View { Logger.shared.log("Updating reading progress: \(roundedProgress) for \(chapterHref), title: \(novelTitle), image: \(imageUrl)", type: "Debug") let validHtmlContent = (!htmlContent.isEmpty && - !htmlContent.contains("undefined") && - htmlContent.count > 50) ? htmlContent : nil + !htmlContent.contains("undefined") && + htmlContent.count > 50) ? htmlContent : nil if validHtmlContent == nil && !htmlContent.isEmpty { Logger.shared.log("Not caching HTML content as it appears invalid", type: "Warning") @@ -1160,6 +1163,16 @@ struct HTMLView: UIViewRepresentable { } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + Logger.shared.log("WebView finished loading navigation", type: "Debug") + + webView.evaluateJavaScript("document.body.innerText.length") { result, error in + if let textLength = result as? Int { + Logger.shared.log("WebView loaded content with text length: \(textLength)", type: "Debug") + } else { + Logger.shared.log("WebView error checking content length: \(error?.localizedDescription ?? "Unknown error")", type: "Error") + } + } + if let href = parent.chapterHref { let savedPosition = UserDefaults.standard.double(forKey: "scrollPosition_\(href)") if savedPosition > 0.01 { @@ -1177,6 +1190,14 @@ struct HTMLView: UIViewRepresentable { startProgressTracking(webView: webView) } + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + Logger.shared.log("WebView navigation failed: \(error.localizedDescription)", type: "Error") + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + Logger.shared.log("WebView provisional navigation failed: \(error.localizedDescription)", type: "Error") + } + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { if message.name == "scrollHandler", let webView = self.webView { updateReadingProgress(webView: webView) @@ -1295,7 +1316,7 @@ struct HTMLView: UIViewRepresentable { } } } - + func makeUIView(context: Context) -> WKWebView { let webView = WKWebView() webView.backgroundColor = .clear @@ -1329,9 +1350,12 @@ struct HTMLView: UIViewRepresentable { } guard !htmlContent.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + Logger.shared.log("HTMLView: Empty HTML content, skipping update", type: "Warning") return } + Logger.shared.log("HTMLView: Updating with content length: \(htmlContent.count)", type: "Debug") + let contentChanged = coordinator.lastHtmlContent != htmlContent let fontSizeChanged = coordinator.lastFontSize != fontSize let fontFamilyChanged = coordinator.lastFontFamily != fontFamily @@ -1342,7 +1366,7 @@ struct HTMLView: UIViewRepresentable { let colorChanged = coordinator.lastColorPreset != colorPreset.name if contentChanged || fontSizeChanged || fontFamilyChanged || fontWeightChanged || - alignmentChanged || lineSpacingChanged || marginChanged || colorChanged { + alignmentChanged || lineSpacingChanged || marginChanged || colorChanged { let htmlTemplate = """