From e2586b2cb682dc6182b8699db748d97a3e7872ee Mon Sep 17 00:00:00 2001 From: cranci1 <100066266+cranci1@users.noreply.github.com> Date: Sat, 12 Jul 2025 19:09:42 +0200 Subject: [PATCH] yep this are peak fixes --- .../ContinueReadingItem.swift | 6 +- .../JSLoader/JSController-Novel.swift | 546 +++++++++--------- Sora/Views/MediaInfoView/MediaInfoView.swift | 83 ++- Sora/Views/ReaderView/ReaderView.swift | 200 ++++--- 4 files changed, 403 insertions(+), 432 deletions(-) diff --git a/Sora/MediaUtils/ContinueWatching/ContinueReadingItem.swift b/Sora/MediaUtils/ContinueWatching/ContinueReadingItem.swift index ecca9ba..f0aedf5 100644 --- a/Sora/MediaUtils/ContinueWatching/ContinueReadingItem.swift +++ b/Sora/MediaUtils/ContinueWatching/ContinueReadingItem.swift @@ -14,7 +14,7 @@ struct ContinueReadingItem: Identifiable, Codable { let chapterNumber: Int let imageUrl: String let href: String - let moduleId: String + let moduleId: UUID let progress: Double let totalChapters: Int let lastReadDate: Date @@ -27,7 +27,7 @@ struct ContinueReadingItem: Identifiable, Codable { chapterNumber: Int, imageUrl: String, href: String, - moduleId: String, + moduleId: UUID, progress: Double = 0.0, totalChapters: Int = 0, lastReadDate: Date = Date(), @@ -45,4 +45,4 @@ struct ContinueReadingItem: Identifiable, Codable { self.lastReadDate = lastReadDate self.cachedHtml = cachedHtml } -} \ No newline at end of file +} diff --git a/Sora/Utlis & Misc/JSLoader/JSController-Novel.swift b/Sora/Utlis & Misc/JSLoader/JSController-Novel.swift index 12d04c4..01aa397 100644 --- a/Sora/Utlis & Misc/JSLoader/JSController-Novel.swift +++ b/Sora/Utlis & Misc/JSLoader/JSController-Novel.swift @@ -32,277 +32,233 @@ enum JSError: Error { } extension JSController { - @MainActor - func extractChapters(moduleId: String, href: String) async throws -> [[String: Any]] { - guard ModuleManager().modules.first(where: { $0.id.uuidString == moduleId }) != nil else { - throw JSError.moduleNotFound + @MainActor func extractChapters(moduleId: UUID, href: String, completion: @escaping ([[String: Any]]) -> Void) { + guard ModuleManager().modules.first(where: { $0.id == moduleId }) != nil else { + Logger.shared.log("Module not found for ID: \(moduleId)", type: "Error") + completion([]) + return } - return await withCheckedContinuation { (continuation: CheckedContinuation<[[String: Any]], Never>) in - DispatchQueue.main.async { [weak self] in - guard let self = self else { - continuation.resume(returning: []) - return - } - guard let extractChaptersFunction = self.context.objectForKeyedSubscript("extractChapters") else { - Logger.shared.log("extractChapters: function not found", type: "Error") - continuation.resume(returning: []) - return - } - let result = extractChaptersFunction.call(withArguments: [href]) - if result?.isUndefined == true || result == nil { - Logger.shared.log("extractChapters: result is undefined or nil", type: "Error") - continuation.resume(returning: []) - return - } - if let result = result, result.hasProperty("then") { - let group = DispatchGroup() - group.enter() - var chaptersArr: [[String: Any]] = [] - var hasLeftGroup = false - let groupQueue = DispatchQueue(label: "extractChapters.group") - - let thenBlock: @convention(block) (JSValue) -> Void = { jsValue in - Logger.shared.log("extractChapters thenBlock: \(jsValue)", type: "Debug") - groupQueue.sync { - guard !hasLeftGroup else { - Logger.shared.log("extractChapters: thenBlock called but group already left", type: "Debug") - return - } - hasLeftGroup = true - - if let arr = jsValue.toArray() as? [[String: Any]] { - Logger.shared.log("extractChapters: parsed as array, count = \(arr.count)", type: "Debug") - chaptersArr = arr - } else if let jsonString = jsValue.toString(), let data = jsonString.data(using: .utf8) { - do { - if let arr = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] { - Logger.shared.log("extractChapters: parsed as JSON string, count = \(arr.count)", type: "Debug") - chaptersArr = arr - } else { - Logger.shared.log("extractChapters: JSON string did not parse to array", type: "Error") - } - } catch { - Logger.shared.log("JSON parsing error of extractChapters: \(error)", type: "Error") + DispatchQueue.main.async { [weak self] in + guard let self = self else { + completion([]) + return + } + + guard let extractChaptersFunction = self.context.objectForKeyedSubscript("extractChapters") else { + Logger.shared.log("extractChapters: function not found", type: "Error") + completion([]) + return + } + + let result = extractChaptersFunction.call(withArguments: [href]) + if result?.isUndefined == true || result == nil { + Logger.shared.log("extractChapters: result is undefined or nil", type: "Error") + completion([]) + return + } + + if let result = result, result.hasProperty("then") { + let group = DispatchGroup() + group.enter() + var chaptersArr: [[String: Any]] = [] + var hasLeftGroup = false + let groupQueue = DispatchQueue(label: "extractChapters.group") + + let thenBlock: @convention(block) (JSValue) -> Void = { jsValue in + Logger.shared.log("extractChapters thenBlock: \(jsValue)", type: "Debug") + groupQueue.sync { + guard !hasLeftGroup else { + Logger.shared.log("extractChapters: thenBlock called but group already left", type: "Debug") + return + } + hasLeftGroup = true + + if let arr = jsValue.toArray() as? [[String: Any]] { + Logger.shared.log("extractChapters: parsed as array, count = \(arr.count)", type: "Debug") + chaptersArr = arr + } else if let jsonString = jsValue.toString(), let data = jsonString.data(using: .utf8) { + do { + if let arr = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] { + Logger.shared.log("extractChapters: parsed as JSON string, count = \(arr.count)", type: "Debug") + chaptersArr = arr + } else { + Logger.shared.log("extractChapters: JSON string did not parse to array", type: "Error") } - } else { - Logger.shared.log("extractChapters: could not parse result", type: "Error") + } catch { + Logger.shared.log("JSON parsing error of extractChapters: \(error)", type: "Error") } - group.leave() + } else { + Logger.shared.log("extractChapters: could not parse result", type: "Error") } + group.leave() } - let catchBlock: @convention(block) (JSValue) -> Void = { jsValue in - Logger.shared.log("extractChapters catchBlock: \(jsValue)", type: "Error") - groupQueue.sync { - guard !hasLeftGroup else { - Logger.shared.log("extractChapters: catchBlock called but group already left", type: "Debug") - return - } - hasLeftGroup = true - group.leave() + } + let catchBlock: @convention(block) (JSValue) -> Void = { jsValue in + Logger.shared.log("extractChapters catchBlock: \(jsValue)", type: "Error") + groupQueue.sync { + guard !hasLeftGroup else { + Logger.shared.log("extractChapters: catchBlock called but group already left", type: "Debug") + return } + hasLeftGroup = true + group.leave() } - result.invokeMethod("then", withArguments: [thenBlock]) - result.invokeMethod("catch", withArguments: [catchBlock]) - group.notify(queue: .main) { - continuation.resume(returning: chaptersArr) + } + result.invokeMethod("then", withArguments: [thenBlock]) + result.invokeMethod("catch", withArguments: [catchBlock]) + group.notify(queue: .main) { + completion(chaptersArr) + } + } else { + if let arr = result?.toArray() as? [[String: Any]] { + Logger.shared.log("extractChapters: direct array, count = \(arr.count)", type: "Debug") + completion(arr) + } else if let jsonString = result?.toString(), let data = jsonString.data(using: .utf8) { + do { + if let arr = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] { + Logger.shared.log("extractChapters: direct JSON string, count = \(arr.count)", type: "Debug") + completion(arr) + } else { + Logger.shared.log("extractChapters: direct JSON string did not parse to array", type: "Error") + completion([]) + } + } catch { + Logger.shared.log("JSON parsing error of extractChapters: \(error)", type: "Error") + completion([]) } } else { - if let arr = result?.toArray() as? [[String: Any]] { - Logger.shared.log("extractChapters: direct array, count = \(arr.count)", type: "Debug") - continuation.resume(returning: arr) - } else if let jsonString = result?.toString(), let data = jsonString.data(using: .utf8) { - do { - if let arr = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] { - Logger.shared.log("extractChapters: direct JSON string, count = \(arr.count)", type: "Debug") - continuation.resume(returning: arr) - } else { - Logger.shared.log("extractChapters: direct JSON string did not parse to array", type: "Error") - continuation.resume(returning: []) - } - } catch { - Logger.shared.log("JSON parsing error of extractChapters: \(error)", type: "Error") - continuation.resume(returning: []) - } - } else { - Logger.shared.log("extractChapters: could not parse direct result", type: "Error") - continuation.resume(returning: []) - } + Logger.shared.log("extractChapters: could not parse direct result", type: "Error") + completion([]) } } } } - @MainActor - func extractText(moduleId: String, href: String) async throws -> String { - guard let module = ModuleManager().modules.first(where: { $0.id.uuidString == moduleId }) else { - throw JSError.moduleNotFound + @MainActor func extractText(moduleId: UUID, href: String, completion: @escaping (Result) -> Void) { + guard let module = ModuleManager().modules.first(where: { $0.id == moduleId }) else { + completion(.failure(JSError.moduleNotFound)) + return } - return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - let workItem = DispatchWorkItem { [weak self] in - guard let self = self else { - continuation.resume(throwing: JSError.invalidResponse) - return - } - - if self.context.objectForKeyedSubscript("extractText") == nil { - Logger.shared.log("extractText function not found, attempting to load module script", type: "Debug") - do { - let moduleContent = try ModuleManager().getModuleContent(module) - self.loadScript(moduleContent) - Logger.shared.log("Successfully loaded module script", type: "Debug") - } catch { - Logger.shared.log("Failed to load module script: \(error)", type: "Error") - } - } - - guard let function = self.context.objectForKeyedSubscript("extractText") else { - Logger.shared.log("extractText function not available after loading module script", type: "Error") - - let task = Task { - return try await self.fetchContentDirectly(from: href) - } - - Task { - do { - let content = try await task.value - continuation.resume(returning: content) - } catch { - continuation.resume(throwing: JSError.invalidResponse) - } - } - return - } - - let result = function.call(withArguments: [href]) - - if let exception = self.context.exception { - Logger.shared.log("Error extracting text: \(exception)", type: "Error") - - let task = Task { - return try await self.fetchContentDirectly(from: href) - } - - Task { - do { - let content = try await task.value - continuation.resume(returning: content) - } catch { - continuation.resume(throwing: JSError.jsException(exception.toString() ?? "Unknown JS error")) - } - } - return - } - - if let result = result, result.hasProperty("then") { - let group = DispatchGroup() - group.enter() - var extractedText = "" - var extractError: Error? = nil - var hasLeftGroup = false - let groupQueue = DispatchQueue(label: "extractText.group") - - let thenBlock: @convention(block) (JSValue) -> Void = { jsValue in - Logger.shared.log("extractText thenBlock: received value", type: "Debug") - groupQueue.sync { - guard !hasLeftGroup else { - Logger.shared.log("extractText: thenBlock called but group already left", type: "Debug") - return - } - hasLeftGroup = true - - if let text = jsValue.toString(), !text.isEmpty { - Logger.shared.log("extractText: successfully extracted text", type: "Debug") - extractedText = text - } else { - extractError = JSError.emptyContent - } - group.leave() - } - } - - let catchBlock: @convention(block) (JSValue) -> Void = { jsValue in - Logger.shared.log("extractText catchBlock: \(jsValue)", type: "Error") - groupQueue.sync { - guard !hasLeftGroup else { - Logger.shared.log("extractText: catchBlock called but group already left", type: "Debug") - return - } - hasLeftGroup = true - - if extractedText.isEmpty { - extractError = JSError.jsException(jsValue.toString() ?? "Unknown error") - } - group.leave() - } - } - - result.invokeMethod("then", withArguments: [thenBlock]) - result.invokeMethod("catch", withArguments: [catchBlock]) - - let notifyWorkItem = DispatchWorkItem { - if !extractedText.isEmpty { - continuation.resume(returning: extractedText) - } else if extractError != nil { - let fetchTask = Task { - return try await self.fetchContentDirectly(from: href) - } - - Task { - do { - let content = try await fetchTask.value - continuation.resume(returning: content) - } catch { - continuation.resume(throwing: error) - } - } - } else { - let fetchTask = Task { - return try await self.fetchContentDirectly(from: href) - } - - Task { - do { - let content = try await fetchTask.value - continuation.resume(returning: content) - } catch _ { - continuation.resume(throwing: JSError.emptyContent) - } - } - } - } - - group.notify(queue: .main, work: notifyWorkItem) - } else { - if let text = result?.toString(), !text.isEmpty { - Logger.shared.log("extractText: direct string result", type: "Debug") - continuation.resume(returning: text) - } else { - Logger.shared.log("extractText: could not parse direct result, trying direct fetch", type: "Error") - let task = Task { - return try await self.fetchContentDirectly(from: href) - } - - Task { - do { - let content = try await task.value - continuation.resume(returning: content) - } catch { - continuation.resume(throwing: JSError.emptyContent) - } - } - } + let workItem = DispatchWorkItem { [weak self] in + guard let self = self else { + completion(.failure(JSError.invalidResponse)) + return + } + + if self.context.objectForKeyedSubscript("extractText") == nil { + Logger.shared.log("extractText function not found, attempting to load module script", type: "Debug") + do { + let moduleContent = try ModuleManager().getModuleContent(module) + self.loadScript(moduleContent) + Logger.shared.log("Successfully loaded module script", type: "Debug") + } catch { + Logger.shared.log("Failed to load module script: \(error)", type: "Error") } } - DispatchQueue.main.async(execute: workItem) + guard let function = self.context.objectForKeyedSubscript("extractText") else { + Logger.shared.log("extractText function not available after loading module script", type: "Error") + + self.fetchContentDirectly(from: href) { result in + completion(result) + } + return + } + + let result = function.call(withArguments: [href]) + + if let exception = self.context.exception { + Logger.shared.log("Error extracting text: \(exception)", type: "Error") + + self.fetchContentDirectly(from: href) { result in + completion(result) + } + return + } + + if let result = result, result.hasProperty("then") { + let group = DispatchGroup() + group.enter() + var extractedText = "" + var extractError: Error? = nil + var hasLeftGroup = false + let groupQueue = DispatchQueue(label: "extractText.group") + + let thenBlock: @convention(block) (JSValue) -> Void = { jsValue in + Logger.shared.log("extractText thenBlock: received value", type: "Debug") + groupQueue.sync { + guard !hasLeftGroup else { + Logger.shared.log("extractText: thenBlock called but group already left", type: "Debug") + return + } + hasLeftGroup = true + + if let text = jsValue.toString(), !text.isEmpty { + Logger.shared.log("extractText: successfully extracted text", type: "Debug") + extractedText = text + } else { + extractError = JSError.emptyContent + } + group.leave() + } + } + + let catchBlock: @convention(block) (JSValue) -> Void = { jsValue in + Logger.shared.log("extractText catchBlock: \(jsValue)", type: "Error") + groupQueue.sync { + guard !hasLeftGroup else { + Logger.shared.log("extractText: catchBlock called but group already left", type: "Debug") + return + } + hasLeftGroup = true + + if extractedText.isEmpty { + extractError = JSError.jsException(jsValue.toString() ?? "Unknown error") + } + group.leave() + } + } + + result.invokeMethod("then", withArguments: [thenBlock]) + result.invokeMethod("catch", withArguments: [catchBlock]) + + let notifyWorkItem = DispatchWorkItem { + if !extractedText.isEmpty { + completion(.success(extractedText)) + } else if extractError != nil { + self.fetchContentDirectly(from: href) { result in + completion(result) + } + } else { + self.fetchContentDirectly(from: href) { result in + completion(result) + } + } + } + + group.notify(queue: .main, work: notifyWorkItem) + } else { + if let text = result?.toString(), !text.isEmpty { + Logger.shared.log("extractText: direct string result", type: "Debug") + completion(.success(text)) + } else { + Logger.shared.log("extractText: could not parse direct result, trying direct fetch", type: "Error") + self.fetchContentDirectly(from: href) { result in + completion(result) + } + } + } } + + DispatchQueue.main.async(execute: workItem) } - private func fetchContentDirectly(from url: String) async throws -> String { + private func fetchContentDirectly(from url: String, completion: @escaping (Result) -> Void) { guard let url = URL(string: url) else { - throw JSError.invalidResponse + completion(.failure(JSError.invalidResponse)) + return } var request = URLRequest(url: url) @@ -311,46 +267,64 @@ extension JSController { Logger.shared.log("Attempting direct fetch from: \(url.absoluteString)", type: "Debug") - let (data, response) = try await URLSession.shared.data(for: request) - - guard let httpResponse = response as? HTTPURLResponse, - (200...299).contains(httpResponse.statusCode) else { - Logger.shared.log("Direct fetch failed with status code: \((response as? HTTPURLResponse)?.statusCode ?? -1)", type: "Error") - throw JSError.invalidResponse + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + DispatchQueue.main.async { + Logger.shared.log("Direct fetch error: \(error.localizedDescription)", type: "Error") + completion(.failure(JSError.invalidResponse)) + } + return + } + + guard let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { + DispatchQueue.main.async { + Logger.shared.log("Direct fetch failed with status code: \((response as? HTTPURLResponse)?.statusCode ?? -1)", type: "Error") + completion(.failure(JSError.invalidResponse)) + } + return + } + + guard let data = data, let htmlString = String(data: data, encoding: .utf8) else { + DispatchQueue.main.async { + Logger.shared.log("Failed to decode response data", type: "Error") + completion(.failure(JSError.invalidResponse)) + } + return + } + + var content = "" + + if let contentRange = htmlString.range(of: "", options: .caseInsensitive) { + let startIndex = contentRange.lowerBound + let endIndex = endRange.upperBound + content = String(htmlString[startIndex..", options: .caseInsensitive, range: contentRange.upperBound..", options: .caseInsensitive, range: contentRange.upperBound..", options: .caseInsensitive) { + let startIndex = bodyRange.lowerBound + let endIndex = endBodyRange.upperBound + content = String(htmlString[startIndex..", options: .caseInsensitive) { - let startIndex = contentRange.lowerBound - let endIndex = endRange.upperBound - content = String(htmlString[startIndex..", options: .caseInsensitive, range: contentRange.upperBound..", options: .caseInsensitive, range: contentRange.upperBound..", options: .caseInsensitive) { - let startIndex = bodyRange.lowerBound - let endIndex = endBodyRange.upperBound - content = String(htmlString[startIndex..) in + await withCheckedContinuation { (continuation: CheckedContinuation) in@Sendable func checkDetails() { Task { @MainActor in if !(self.synopsis.isEmpty && self.aliases.isEmpty && self.airdate.isEmpty) { @@ -1009,7 +985,7 @@ struct MediaInfoView: View { } } while true { - let loaded = await MainActor.run { chaptersLoaded && detailsLoaded } + let loaded = await MainActor.run { detailsLoaded } if loaded { break } try? await Task.sleep(nanoseconds: 100_000_000) } @@ -1433,39 +1409,50 @@ struct MediaInfoView: View { } func fetchDetails() { - Logger.shared.log("fetchDetails: called", type: "Debug") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - Task { - do { - let jsContent = try moduleManager.getModuleContent(module) - jsController.loadScript(jsContent) - - let completion: (Any?, [EpisodeLink]) -> Void = { items, episodes in + do { + let jsContent = try self.moduleManager.getModuleContent(self.module) + self.jsController.loadScript(jsContent) + + let completion: (Any?, [EpisodeLink]) -> Void = { items, episodes in + if self.module.metadata.novel ?? true { + self.jsController.extractChapters(moduleId: self.module.id, href: self.href) { chapters in + DispatchQueue.main.async { + self.handleFetchDetailsResponse(items: chapters, episodes: episodes) + } + } + } else { self.handleFetchDetailsResponse(items: items, episodes: episodes) } - - if module.metadata.asyncJS == true { - jsController.fetchDetailsJS(url: href, completion: completion) - } else { - jsController.fetchDetails(url: href, completion: completion) - } - } catch { - Logger.shared.log("Error loading module: \(error)", type: "Error") - self.isLoading = false - self.isRefetching = false } + + if self.module.metadata.asyncJS == true { + self.jsController.fetchDetailsJS(url: self.href, completion: completion) + } else { + self.jsController.fetchDetails(url: self.href, completion: completion) + } + } catch { + Logger.shared.log("Error loading module: \(error)", type: "Error") + self.isLoading = false + self.isRefetching = false } } } private func handleFetchDetailsResponse(items: Any?, episodes: [EpisodeLink]) { - Logger.shared.log("fetchDetails: items = \(items)", type: "Debug") + 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 { - Logger.shared.log("fetchDetails: (novel) chapters count = \(chapters.count)", type: "Debug") + 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 diff --git a/Sora/Views/ReaderView/ReaderView.swift b/Sora/Views/ReaderView/ReaderView.swift index 80202d8..46e397e 100644 --- a/Sora/Views/ReaderView/ReaderView.swift +++ b/Sora/Views/ReaderView/ReaderView.swift @@ -10,7 +10,7 @@ import WebKit class ChapterNavigator: ObservableObject { static let shared = ChapterNavigator() - @Published var currentChapter: (moduleId: String, href: String, title: String, chapters: [[String: Any]], mediaTitle: String, chapterNumber: Int)? = nil + @Published var currentChapter: (moduleId: UUID, href: String, title: String, chapters: [[String: Any]], mediaTitle: String, chapterNumber: Int)? = nil } extension UserDefaults { @@ -27,7 +27,7 @@ extension UserDefaults { } struct ReaderView: View { - let moduleId: String + let moduleId: UUID let chapterHref: String let chapterTitle: String let chapters: [[String: Any]] @@ -96,7 +96,7 @@ struct ReaderView: View { ) } - init(moduleId: String, chapterHref: String, chapterTitle: String, chapters: [[String: Any]] = [], mediaTitle: String = "Unknown Novel", chapterNumber: Int = 1) { + init(moduleId: UUID, chapterHref: String, chapterTitle: String, chapters: [[String: Any]] = [], mediaTitle: String = "Unknown Novel", chapterNumber: Int = 1) { self.moduleId = moduleId self.chapterHref = chapterHref self.chapterTitle = chapterTitle @@ -114,7 +114,7 @@ struct ReaderView: View { } private func ensureModuleLoaded() { - if let module = ModuleManager().modules.first(where: { $0.id.uuidString == moduleId }) { + if let module = ModuleManager().modules.first(where: { $0.id == moduleId }) { do { let moduleContent = try ModuleManager().getModuleContent(module) JSController.shared.loadScript(moduleContent) @@ -230,6 +230,8 @@ struct ReaderView: View { NotificationCenter.default.post(name: .hideTabBar, object: nil) UserDefaults.standard.set(true, forKey: "isReaderActive") + loadContent() + DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { withAnimation(.easeInOut(duration: 0.6)) { isHeaderVisible = false @@ -304,106 +306,114 @@ struct ReaderView: View { UserDefaults.standard.set(false, forKey: "isReaderActive") setStatusBarHidden(false) } - - .task { - do { - ensureModuleLoaded() + .statusBar(hidden: statusBarHidden) + } + + @MainActor + private func loadContent() { + do { + ensureModuleLoaded() + + if let cachedContent = ContinueReadingManager.shared.getCachedHtml(for: self.chapterHref), + !cachedContent.isEmpty && + !cachedContent.contains("undefined") && + cachedContent.count > 50 { - let isConnected = await NetworkMonitor.shared.ensureNetworkStatusInitialized() - let isOffline = !isConnected + Logger.shared.log("Using cached HTML content for \(self.chapterHref)", type: "Debug") + self.htmlContent = cachedContent + self.isLoading = false - if let cachedContent = ContinueReadingManager.shared.getCachedHtml(for: chapterHref), - !cachedContent.isEmpty && - !cachedContent.contains("undefined") && - cachedContent.count > 50 { - Logger.shared.log("Using cached HTML content for \(chapterHref)", type: "Debug") - htmlContent = cachedContent - isLoading = false - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - withAnimation(.easeInOut(duration: 0.3)) { - isHeaderVisible = false - statusBarHidden = true - setStatusBarHidden(true) - } - } - } else if isOffline { - let offlineError = NSError(domain: "Sora", code: -1009, userInfo: [NSLocalizedDescriptionKey: "No network connection."]) - self.error = offlineError - isLoading = false - return - } else { - Logger.shared.log("Fetching HTML content from network for \(chapterHref)", type: "Debug") - - var content = "" - var attempts = 0 - var lastError: Error? = nil - - while attempts < 3 && (content.isEmpty || content.contains("undefined") || content.count < 50) { - do { - attempts += 1 - content = try await JSController.shared.extractText(moduleId: moduleId, href: chapterHref) - - if content.isEmpty || content.contains("undefined") || content.count < 50 { - Logger.shared.log("Received invalid content on attempt \(attempts), retrying...", type: "Warning") - try await Task.sleep(nanoseconds: 500_000_000) - } - } catch { - lastError = error - Logger.shared.log("Error fetching content on attempt \(attempts): \(error.localizedDescription)", type: "Error") - try await Task.sleep(nanoseconds: 500_000_000) - } - } - - if !content.isEmpty && !content.contains("undefined") && content.count >= 50 { - htmlContent = content - isLoading = false - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - withAnimation(.easeInOut(duration: 0.3)) { - isHeaderVisible = false - statusBarHidden = true - setStatusBarHidden(true) - } - } - - if let cachedContent = ContinueReadingManager.shared.getCachedHtml(for: chapterHref), - cachedContent.isEmpty || cachedContent.contains("undefined") || cachedContent.count < 50 { - let item = ContinueReadingItem( - mediaTitle: mediaTitle, - chapterTitle: chapterTitle, - chapterNumber: chapterNumber, - imageUrl: UserDefaults.standard.string(forKey: "novelImageUrl_\(moduleId)_\(mediaTitle)") ?? "", - href: chapterHref, - moduleId: moduleId, - progress: readingProgress, - totalChapters: chapters.count, - lastReadDate: Date(), - cachedHtml: content - ) - ContinueReadingManager.shared.save(item: item, htmlContent: content) - } - } else if let lastError = lastError { - throw lastError - } else { - throw JSError.emptyContent + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + withAnimation(.easeInOut(duration: 0.3)) { + self.isHeaderVisible = false + self.statusBarHidden = true + self.setStatusBarHidden(true) } } - } catch { + } + } catch { + self.error = error + self.isLoading = false + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + DropManager.shared.showDrop( + title: "Error Loading Content", + subtitle: error.localizedDescription, + duration: 2.0, + icon: UIImage(systemName: "exclamationmark.triangle") + ) + } + } + } + + private func fetchContentWithRetries(attempts: Int, maxAttempts: Int, lastError: Error? = nil) { + guard attempts < maxAttempts else { + if let error = lastError { self.error = error - isLoading = false + } else { + self.error = JSError.emptyContent + } + self.isLoading = false + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + DropManager.shared.showDrop( + title: "Error Loading Content", + subtitle: self.error?.localizedDescription ?? "Failed to load content", + duration: 2.0, + icon: UIImage(systemName: "exclamationmark.triangle") + ) + } + return + } + + JSController.shared.extractText(moduleId: moduleId, href: chapterHref) { result in + switch result { + case .success(let content): + if content.isEmpty || content.contains("undefined") || content.count < 50 { + Logger.shared.log("Received invalid content on attempt \(attempts + 1), retrying...", type: "Warning") + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.fetchContentWithRetries(attempts: attempts + 1, maxAttempts: maxAttempts, lastError: JSError.emptyContent) + } + 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) + } + } + + if let cachedContent = ContinueReadingManager.shared.getCachedHtml(for: self.chapterHref), + cachedContent.isEmpty || cachedContent.contains("undefined") || cachedContent.count < 50 { + let item = ContinueReadingItem( + mediaTitle: self.mediaTitle, + chapterTitle: self.chapterTitle, + chapterNumber: self.chapterNumber, + imageUrl: UserDefaults.standard.string(forKey: "novelImageUrl_\(self.moduleId)_\(self.mediaTitle)") ?? "", + href: self.chapterHref, + moduleId: self.moduleId, + progress: self.readingProgress, + totalChapters: self.chapters.count, + lastReadDate: Date(), + cachedHtml: content + ) + ContinueReadingManager.shared.save(item: item, htmlContent: content) + } + + case .failure(let error): + Logger.shared.log("Error fetching content on attempt \(attempts + 1): \(error.localizedDescription)", type: "Error") DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - DropManager.shared.showDrop( - title: "Error Loading Content", - subtitle: error.localizedDescription, - duration: 2.0, - icon: UIImage(systemName: "exclamationmark.triangle") - ) + self.fetchContentWithRetries(attempts: attempts + 1, maxAttempts: maxAttempts, lastError: error) } } } - .statusBar(hidden: statusBarHidden) } private func stopAutoScroll() { @@ -953,7 +963,7 @@ struct ReaderView: View { UserDefaults.standard.set(roundedProgress, forKey: "readingProgress_\(chapterHref)") - var novelTitle = self.mediaTitle + let novelTitle = self.mediaTitle var currentChapterNumber = 1 var imageUrl = ""