yep this are peak fixes
Some checks are pending
Build and Release / Build IPA (push) Waiting to run
Build and Release / Build Mac Catalyst (push) Waiting to run

This commit is contained in:
cranci1 2025-07-12 19:09:42 +02:00
parent 37a684132a
commit e2586b2cb6
4 changed files with 403 additions and 432 deletions

View file

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

View file

@ -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<String, Error>) -> Void) {
guard let module = ModuleManager().modules.first(where: { $0.id == moduleId }) else {
completion(.failure(JSError.moduleNotFound))
return
}
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<String, Error>) 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<String, Error> {
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<String, Error> {
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<String, Error> {
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<String, Error> {
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<String, Error> {
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<String, Error>) -> 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: "<article", options: .caseInsensitive),
let endRange = htmlString.range(of: "</article>", options: .caseInsensitive) {
let startIndex = contentRange.lowerBound
let endIndex = endRange.upperBound
content = String(htmlString[startIndex..<endIndex])
} else if let contentRange = htmlString.range(of: "<div class=\"chapter-content\"", options: .caseInsensitive),
let endRange = htmlString.range(of: "</div>", options: .caseInsensitive, range: contentRange.upperBound..<htmlString.endIndex) {
let startIndex = contentRange.lowerBound
let endIndex = endRange.upperBound
content = String(htmlString[startIndex..<endIndex])
} else if let contentRange = htmlString.range(of: "<div class=\"content\"", options: .caseInsensitive),
let endRange = htmlString.range(of: "</div>", options: .caseInsensitive, range: contentRange.upperBound..<htmlString.endIndex) {
let startIndex = contentRange.lowerBound
let endIndex = endRange.upperBound
content = String(htmlString[startIndex..<endIndex])
} else if let bodyRange = htmlString.range(of: "<body", options: .caseInsensitive),
let endBodyRange = htmlString.range(of: "</body>", options: .caseInsensitive) {
let startIndex = bodyRange.lowerBound
let endIndex = endBodyRange.upperBound
content = String(htmlString[startIndex..<endIndex])
} else {
content = htmlString
}
DispatchQueue.main.async {
Logger.shared.log("Direct fetch successful, content length: \(content.count)", type: "Debug")
completion(.success(content))
}
}
guard let htmlString = String(data: data, encoding: .utf8) else {
Logger.shared.log("Failed to decode response data", type: "Error")
throw JSError.invalidResponse
}
var content = ""
if let contentRange = htmlString.range(of: "<article", options: .caseInsensitive),
let endRange = htmlString.range(of: "</article>", options: .caseInsensitive) {
let startIndex = contentRange.lowerBound
let endIndex = endRange.upperBound
content = String(htmlString[startIndex..<endIndex])
} else if let contentRange = htmlString.range(of: "<div class=\"chapter-content\"", options: .caseInsensitive),
let endRange = htmlString.range(of: "</div>", options: .caseInsensitive, range: contentRange.upperBound..<htmlString.endIndex) {
let startIndex = contentRange.lowerBound
let endIndex = endRange.upperBound
content = String(htmlString[startIndex..<endIndex])
} else if let contentRange = htmlString.range(of: "<div class=\"content\"", options: .caseInsensitive),
let endRange = htmlString.range(of: "</div>", options: .caseInsensitive, range: contentRange.upperBound..<htmlString.endIndex) {
let startIndex = contentRange.lowerBound
let endIndex = endRange.upperBound
content = String(htmlString[startIndex..<endIndex])
} else if let bodyRange = htmlString.range(of: "<body", options: .caseInsensitive),
let endBodyRange = htmlString.range(of: "</body>", options: .caseInsensitive) {
let startIndex = bodyRange.lowerBound
let endIndex = endBodyRange.upperBound
content = String(htmlString[startIndex..<endIndex])
} else {
content = htmlString
}
Logger.shared.log("Direct fetch successful, content length: \(content.count)", type: "Debug")
return content
task.resume()
}
}

View file

@ -187,14 +187,6 @@ struct MediaInfoView: View {
setupViewOnAppear()
NotificationCenter.default.post(name: .hideTabBar, object: nil)
UserDefaults.standard.set(true, forKey: "isMediaInfoActive")
// swipe back
/*
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first,
let navigationController = window.rootViewController?.children.first as? UINavigationController {
navigationController.interactivePopGestureRecognizer?.isEnabled = false
}
*/
}
.onChange(of: selectedRange) { newValue in
UserDefaults.standard.set(newValue.lowerBound, forKey: selectedRangeKey)
@ -738,7 +730,7 @@ struct MediaInfoView: View {
let title = chapter["title"] as? String {
NavigationLink(
destination: ReaderView(
moduleId: module.id.uuidString,
moduleId: module.id,
chapterHref: href,
chapterTitle: title,
chapters: chapters,
@ -762,13 +754,13 @@ struct MediaInfoView: View {
.simultaneousGesture(TapGesture().onEnded {
UserDefaults.standard.set(true, forKey: "navigatingToReaderView")
ChapterNavigator.shared.currentChapter = (
moduleId: module.id.uuidString,
moduleId: module.id,
href: href,
title: title,
chapters: chapters,
mediaTitle: self.title,
chapterNumber: number
)
) as! (moduleId: UUID, href: String, title: String, chapters: [[String : Any]], mediaTitle: String, chapterNumber: Int)
})
.contextMenu {
Button(action: {
@ -950,13 +942,9 @@ struct MediaInfoView: View {
private func setupInitialData() async {
do {
Logger.shared.log("setupInitialData: module.metadata.novel = \(String(describing: module.metadata.novel))", type: "Debug")
UserDefaults.standard.set(imageUrl, forKey: "mediaInfoImageUrl_\(module.id.uuidString)")
Logger.shared.log("Saved MediaInfoView image URL: \(imageUrl) for module \(module.id.uuidString)", type: "Debug")
if module.metadata.novel == true {
if !hasFetched {
DispatchQueue.main.async {
@ -974,25 +962,13 @@ struct MediaInfoView: View {
}
await withTaskGroup(of: Void.self) { group in
var chaptersLoaded = false
var detailsLoaded = false
group.addTask {
let fetchedChapters = try? await JSController.shared.extractChapters(moduleId: module.id.uuidString, href: href)
await MainActor.run {
if let fetchedChapters = fetchedChapters {
Logger.shared.log("setupInitialData: fetchedChapters count = \(fetchedChapters.count)", type: "Debug")
Logger.shared.log("setupInitialData: fetchedChapters = \(fetchedChapters)", type: "Debug")
self.chapters = fetchedChapters
}
chaptersLoaded = true
}
}
group.addTask {
await MainActor.run {
self.fetchDetails()
}
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) 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

View file

@ -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 = ""