mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
yep this are peak fixes
This commit is contained in:
parent
37a684132a
commit
e2586b2cb6
4 changed files with 403 additions and 432 deletions
|
|
@ -14,7 +14,7 @@ struct ContinueReadingItem: Identifiable, Codable {
|
||||||
let chapterNumber: Int
|
let chapterNumber: Int
|
||||||
let imageUrl: String
|
let imageUrl: String
|
||||||
let href: String
|
let href: String
|
||||||
let moduleId: String
|
let moduleId: UUID
|
||||||
let progress: Double
|
let progress: Double
|
||||||
let totalChapters: Int
|
let totalChapters: Int
|
||||||
let lastReadDate: Date
|
let lastReadDate: Date
|
||||||
|
|
@ -27,7 +27,7 @@ struct ContinueReadingItem: Identifiable, Codable {
|
||||||
chapterNumber: Int,
|
chapterNumber: Int,
|
||||||
imageUrl: String,
|
imageUrl: String,
|
||||||
href: String,
|
href: String,
|
||||||
moduleId: String,
|
moduleId: UUID,
|
||||||
progress: Double = 0.0,
|
progress: Double = 0.0,
|
||||||
totalChapters: Int = 0,
|
totalChapters: Int = 0,
|
||||||
lastReadDate: Date = Date(),
|
lastReadDate: Date = Date(),
|
||||||
|
|
@ -45,4 +45,4 @@ struct ContinueReadingItem: Identifiable, Codable {
|
||||||
self.lastReadDate = lastReadDate
|
self.lastReadDate = lastReadDate
|
||||||
self.cachedHtml = cachedHtml
|
self.cachedHtml = cachedHtml
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,277 +32,233 @@ enum JSError: Error {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension JSController {
|
extension JSController {
|
||||||
@MainActor
|
@MainActor func extractChapters(moduleId: UUID, href: String, completion: @escaping ([[String: Any]]) -> Void) {
|
||||||
func extractChapters(moduleId: String, href: String) async throws -> [[String: Any]] {
|
guard ModuleManager().modules.first(where: { $0.id == moduleId }) != nil else {
|
||||||
guard ModuleManager().modules.first(where: { $0.id.uuidString == moduleId }) != nil else {
|
Logger.shared.log("Module not found for ID: \(moduleId)", type: "Error")
|
||||||
throw JSError.moduleNotFound
|
completion([])
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return await withCheckedContinuation { (continuation: CheckedContinuation<[[String: Any]], Never>) in
|
DispatchQueue.main.async { [weak self] in
|
||||||
DispatchQueue.main.async { [weak self] in
|
guard let self = self else {
|
||||||
guard let self = self else {
|
completion([])
|
||||||
continuation.resume(returning: [])
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
guard let extractChaptersFunction = self.context.objectForKeyedSubscript("extractChapters") else {
|
guard let extractChaptersFunction = self.context.objectForKeyedSubscript("extractChapters") else {
|
||||||
Logger.shared.log("extractChapters: function not found", type: "Error")
|
Logger.shared.log("extractChapters: function not found", type: "Error")
|
||||||
continuation.resume(returning: [])
|
completion([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let result = extractChaptersFunction.call(withArguments: [href])
|
|
||||||
if result?.isUndefined == true || result == nil {
|
let result = extractChaptersFunction.call(withArguments: [href])
|
||||||
Logger.shared.log("extractChapters: result is undefined or nil", type: "Error")
|
if result?.isUndefined == true || result == nil {
|
||||||
continuation.resume(returning: [])
|
Logger.shared.log("extractChapters: result is undefined or nil", type: "Error")
|
||||||
return
|
completion([])
|
||||||
}
|
return
|
||||||
if let result = result, result.hasProperty("then") {
|
}
|
||||||
let group = DispatchGroup()
|
|
||||||
group.enter()
|
if let result = result, result.hasProperty("then") {
|
||||||
var chaptersArr: [[String: Any]] = []
|
let group = DispatchGroup()
|
||||||
var hasLeftGroup = false
|
group.enter()
|
||||||
let groupQueue = DispatchQueue(label: "extractChapters.group")
|
var chaptersArr: [[String: Any]] = []
|
||||||
|
var hasLeftGroup = false
|
||||||
let thenBlock: @convention(block) (JSValue) -> Void = { jsValue in
|
let groupQueue = DispatchQueue(label: "extractChapters.group")
|
||||||
Logger.shared.log("extractChapters thenBlock: \(jsValue)", type: "Debug")
|
|
||||||
groupQueue.sync {
|
let thenBlock: @convention(block) (JSValue) -> Void = { jsValue in
|
||||||
guard !hasLeftGroup else {
|
Logger.shared.log("extractChapters thenBlock: \(jsValue)", type: "Debug")
|
||||||
Logger.shared.log("extractChapters: thenBlock called but group already left", type: "Debug")
|
groupQueue.sync {
|
||||||
return
|
guard !hasLeftGroup else {
|
||||||
}
|
Logger.shared.log("extractChapters: thenBlock called but group already left", type: "Debug")
|
||||||
hasLeftGroup = true
|
return
|
||||||
|
}
|
||||||
if let arr = jsValue.toArray() as? [[String: Any]] {
|
hasLeftGroup = true
|
||||||
Logger.shared.log("extractChapters: parsed as array, count = \(arr.count)", type: "Debug")
|
|
||||||
chaptersArr = arr
|
if let arr = jsValue.toArray() as? [[String: Any]] {
|
||||||
} else if let jsonString = jsValue.toString(), let data = jsonString.data(using: .utf8) {
|
Logger.shared.log("extractChapters: parsed as array, count = \(arr.count)", type: "Debug")
|
||||||
do {
|
chaptersArr = arr
|
||||||
if let arr = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] {
|
} else if let jsonString = jsValue.toString(), let data = jsonString.data(using: .utf8) {
|
||||||
Logger.shared.log("extractChapters: parsed as JSON string, count = \(arr.count)", type: "Debug")
|
do {
|
||||||
chaptersArr = arr
|
if let arr = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] {
|
||||||
} else {
|
Logger.shared.log("extractChapters: parsed as JSON string, count = \(arr.count)", type: "Debug")
|
||||||
Logger.shared.log("extractChapters: JSON string did not parse to array", type: "Error")
|
chaptersArr = arr
|
||||||
}
|
} else {
|
||||||
} catch {
|
Logger.shared.log("extractChapters: JSON string did not parse to array", type: "Error")
|
||||||
Logger.shared.log("JSON parsing error of extractChapters: \(error)", type: "Error")
|
|
||||||
}
|
}
|
||||||
} else {
|
} catch {
|
||||||
Logger.shared.log("extractChapters: could not parse result", type: "Error")
|
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")
|
let catchBlock: @convention(block) (JSValue) -> Void = { jsValue in
|
||||||
groupQueue.sync {
|
Logger.shared.log("extractChapters catchBlock: \(jsValue)", type: "Error")
|
||||||
guard !hasLeftGroup else {
|
groupQueue.sync {
|
||||||
Logger.shared.log("extractChapters: catchBlock called but group already left", type: "Debug")
|
guard !hasLeftGroup else {
|
||||||
return
|
Logger.shared.log("extractChapters: catchBlock called but group already left", type: "Debug")
|
||||||
}
|
return
|
||||||
hasLeftGroup = true
|
|
||||||
group.leave()
|
|
||||||
}
|
}
|
||||||
|
hasLeftGroup = true
|
||||||
|
group.leave()
|
||||||
}
|
}
|
||||||
result.invokeMethod("then", withArguments: [thenBlock])
|
}
|
||||||
result.invokeMethod("catch", withArguments: [catchBlock])
|
result.invokeMethod("then", withArguments: [thenBlock])
|
||||||
group.notify(queue: .main) {
|
result.invokeMethod("catch", withArguments: [catchBlock])
|
||||||
continuation.resume(returning: chaptersArr)
|
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 {
|
} else {
|
||||||
if let arr = result?.toArray() as? [[String: Any]] {
|
Logger.shared.log("extractChapters: could not parse direct result", type: "Error")
|
||||||
Logger.shared.log("extractChapters: direct array, count = \(arr.count)", type: "Debug")
|
completion([])
|
||||||
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: [])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor func extractText(moduleId: UUID, href: String, completion: @escaping (Result<String, Error>) -> Void) {
|
||||||
func extractText(moduleId: String, href: String) async throws -> String {
|
guard let module = ModuleManager().modules.first(where: { $0.id == moduleId }) else {
|
||||||
guard let module = ModuleManager().modules.first(where: { $0.id.uuidString == moduleId }) else {
|
completion(.failure(JSError.moduleNotFound))
|
||||||
throw JSError.moduleNotFound
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<String, Error>) in
|
let workItem = DispatchWorkItem { [weak self] in
|
||||||
let workItem = DispatchWorkItem { [weak self] in
|
guard let self = self else {
|
||||||
guard let self = self else {
|
completion(.failure(JSError.invalidResponse))
|
||||||
continuation.resume(throwing: JSError.invalidResponse)
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
if self.context.objectForKeyedSubscript("extractText") == nil {
|
||||||
if self.context.objectForKeyedSubscript("extractText") == nil {
|
Logger.shared.log("extractText function not found, attempting to load module script", type: "Debug")
|
||||||
Logger.shared.log("extractText function not found, attempting to load module script", type: "Debug")
|
do {
|
||||||
do {
|
let moduleContent = try ModuleManager().getModuleContent(module)
|
||||||
let moduleContent = try ModuleManager().getModuleContent(module)
|
self.loadScript(moduleContent)
|
||||||
self.loadScript(moduleContent)
|
Logger.shared.log("Successfully loaded module script", type: "Debug")
|
||||||
Logger.shared.log("Successfully loaded module script", type: "Debug")
|
} catch {
|
||||||
} catch {
|
Logger.shared.log("Failed to load module script: \(error)", type: "Error")
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
guard let url = URL(string: url) else {
|
||||||
throw JSError.invalidResponse
|
completion(.failure(JSError.invalidResponse))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
|
|
@ -311,46 +267,64 @@ extension JSController {
|
||||||
|
|
||||||
Logger.shared.log("Attempting direct fetch from: \(url.absoluteString)", type: "Debug")
|
Logger.shared.log("Attempting direct fetch from: \(url.absoluteString)", type: "Debug")
|
||||||
|
|
||||||
let (data, response) = try await URLSession.shared.data(for: request)
|
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
|
if let error = error {
|
||||||
guard let httpResponse = response as? HTTPURLResponse,
|
DispatchQueue.main.async {
|
||||||
(200...299).contains(httpResponse.statusCode) else {
|
Logger.shared.log("Direct fetch error: \(error.localizedDescription)", type: "Error")
|
||||||
Logger.shared.log("Direct fetch failed with status code: \((response as? HTTPURLResponse)?.statusCode ?? -1)", type: "Error")
|
completion(.failure(JSError.invalidResponse))
|
||||||
throw 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 {
|
task.resume()
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -187,14 +187,6 @@ struct MediaInfoView: View {
|
||||||
setupViewOnAppear()
|
setupViewOnAppear()
|
||||||
NotificationCenter.default.post(name: .hideTabBar, object: nil)
|
NotificationCenter.default.post(name: .hideTabBar, object: nil)
|
||||||
UserDefaults.standard.set(true, forKey: "isMediaInfoActive")
|
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
|
.onChange(of: selectedRange) { newValue in
|
||||||
UserDefaults.standard.set(newValue.lowerBound, forKey: selectedRangeKey)
|
UserDefaults.standard.set(newValue.lowerBound, forKey: selectedRangeKey)
|
||||||
|
|
@ -738,7 +730,7 @@ struct MediaInfoView: View {
|
||||||
let title = chapter["title"] as? String {
|
let title = chapter["title"] as? String {
|
||||||
NavigationLink(
|
NavigationLink(
|
||||||
destination: ReaderView(
|
destination: ReaderView(
|
||||||
moduleId: module.id.uuidString,
|
moduleId: module.id,
|
||||||
chapterHref: href,
|
chapterHref: href,
|
||||||
chapterTitle: title,
|
chapterTitle: title,
|
||||||
chapters: chapters,
|
chapters: chapters,
|
||||||
|
|
@ -762,13 +754,13 @@ struct MediaInfoView: View {
|
||||||
.simultaneousGesture(TapGesture().onEnded {
|
.simultaneousGesture(TapGesture().onEnded {
|
||||||
UserDefaults.standard.set(true, forKey: "navigatingToReaderView")
|
UserDefaults.standard.set(true, forKey: "navigatingToReaderView")
|
||||||
ChapterNavigator.shared.currentChapter = (
|
ChapterNavigator.shared.currentChapter = (
|
||||||
moduleId: module.id.uuidString,
|
moduleId: module.id,
|
||||||
href: href,
|
href: href,
|
||||||
title: title,
|
title: title,
|
||||||
chapters: chapters,
|
chapters: chapters,
|
||||||
mediaTitle: self.title,
|
mediaTitle: self.title,
|
||||||
chapterNumber: number
|
chapterNumber: number
|
||||||
)
|
) as! (moduleId: UUID, href: String, title: String, chapters: [[String : Any]], mediaTitle: String, chapterNumber: Int)
|
||||||
})
|
})
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
|
@ -950,13 +942,9 @@ struct MediaInfoView: View {
|
||||||
|
|
||||||
private func setupInitialData() async {
|
private func setupInitialData() async {
|
||||||
do {
|
do {
|
||||||
Logger.shared.log("setupInitialData: module.metadata.novel = \(String(describing: module.metadata.novel))", type: "Debug")
|
|
||||||
|
|
||||||
UserDefaults.standard.set(imageUrl, forKey: "mediaInfoImageUrl_\(module.id.uuidString)")
|
UserDefaults.standard.set(imageUrl, forKey: "mediaInfoImageUrl_\(module.id.uuidString)")
|
||||||
Logger.shared.log("Saved MediaInfoView image URL: \(imageUrl) for module \(module.id.uuidString)", type: "Debug")
|
Logger.shared.log("Saved MediaInfoView image URL: \(imageUrl) for module \(module.id.uuidString)", type: "Debug")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if module.metadata.novel == true {
|
if module.metadata.novel == true {
|
||||||
if !hasFetched {
|
if !hasFetched {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|
@ -974,25 +962,13 @@ struct MediaInfoView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
await withTaskGroup(of: Void.self) { group in
|
await withTaskGroup(of: Void.self) { group in
|
||||||
var chaptersLoaded = false
|
|
||||||
var detailsLoaded = 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 {
|
group.addTask {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
self.fetchDetails()
|
self.fetchDetails()
|
||||||
}
|
}
|
||||||
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
|
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in@Sendable
|
||||||
func checkDetails() {
|
func checkDetails() {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
if !(self.synopsis.isEmpty && self.aliases.isEmpty && self.airdate.isEmpty) {
|
if !(self.synopsis.isEmpty && self.aliases.isEmpty && self.airdate.isEmpty) {
|
||||||
|
|
@ -1009,7 +985,7 @@ struct MediaInfoView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while true {
|
while true {
|
||||||
let loaded = await MainActor.run { chaptersLoaded && detailsLoaded }
|
let loaded = await MainActor.run { detailsLoaded }
|
||||||
if loaded { break }
|
if loaded { break }
|
||||||
try? await Task.sleep(nanoseconds: 100_000_000)
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
||||||
}
|
}
|
||||||
|
|
@ -1433,39 +1409,50 @@ struct MediaInfoView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchDetails() {
|
func fetchDetails() {
|
||||||
Logger.shared.log("fetchDetails: called", type: "Debug")
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
Task {
|
do {
|
||||||
do {
|
let jsContent = try self.moduleManager.getModuleContent(self.module)
|
||||||
let jsContent = try moduleManager.getModuleContent(module)
|
self.jsController.loadScript(jsContent)
|
||||||
jsController.loadScript(jsContent)
|
|
||||||
|
let completion: (Any?, [EpisodeLink]) -> Void = { items, episodes in
|
||||||
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)
|
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]) {
|
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")
|
Logger.shared.log("fetchDetails: episodes = \(episodes)", type: "Debug")
|
||||||
|
|
||||||
processItemsResponse(items)
|
processItemsResponse(items)
|
||||||
|
|
||||||
if module.metadata.novel ?? false {
|
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 {
|
} else {
|
||||||
Logger.shared.log("fetchDetails: (episodes) episodes count = \(episodes.count)", type: "Debug")
|
Logger.shared.log("fetchDetails: (episodes) episodes count = \(episodes.count)", type: "Debug")
|
||||||
episodeLinks = episodes
|
episodeLinks = episodes
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import WebKit
|
||||||
|
|
||||||
class ChapterNavigator: ObservableObject {
|
class ChapterNavigator: ObservableObject {
|
||||||
static let shared = ChapterNavigator()
|
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 {
|
extension UserDefaults {
|
||||||
|
|
@ -27,7 +27,7 @@ extension UserDefaults {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ReaderView: View {
|
struct ReaderView: View {
|
||||||
let moduleId: String
|
let moduleId: UUID
|
||||||
let chapterHref: String
|
let chapterHref: String
|
||||||
let chapterTitle: String
|
let chapterTitle: String
|
||||||
let chapters: [[String: Any]]
|
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.moduleId = moduleId
|
||||||
self.chapterHref = chapterHref
|
self.chapterHref = chapterHref
|
||||||
self.chapterTitle = chapterTitle
|
self.chapterTitle = chapterTitle
|
||||||
|
|
@ -114,7 +114,7 @@ struct ReaderView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ensureModuleLoaded() {
|
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 {
|
do {
|
||||||
let moduleContent = try ModuleManager().getModuleContent(module)
|
let moduleContent = try ModuleManager().getModuleContent(module)
|
||||||
JSController.shared.loadScript(moduleContent)
|
JSController.shared.loadScript(moduleContent)
|
||||||
|
|
@ -230,6 +230,8 @@ struct ReaderView: View {
|
||||||
NotificationCenter.default.post(name: .hideTabBar, object: nil)
|
NotificationCenter.default.post(name: .hideTabBar, object: nil)
|
||||||
UserDefaults.standard.set(true, forKey: "isReaderActive")
|
UserDefaults.standard.set(true, forKey: "isReaderActive")
|
||||||
|
|
||||||
|
loadContent()
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||||
withAnimation(.easeInOut(duration: 0.6)) {
|
withAnimation(.easeInOut(duration: 0.6)) {
|
||||||
isHeaderVisible = false
|
isHeaderVisible = false
|
||||||
|
|
@ -304,106 +306,114 @@ struct ReaderView: View {
|
||||||
UserDefaults.standard.set(false, forKey: "isReaderActive")
|
UserDefaults.standard.set(false, forKey: "isReaderActive")
|
||||||
setStatusBarHidden(false)
|
setStatusBarHidden(false)
|
||||||
}
|
}
|
||||||
|
.statusBar(hidden: statusBarHidden)
|
||||||
.task {
|
}
|
||||||
do {
|
|
||||||
ensureModuleLoaded()
|
@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()
|
Logger.shared.log("Using cached HTML content for \(self.chapterHref)", type: "Debug")
|
||||||
let isOffline = !isConnected
|
self.htmlContent = cachedContent
|
||||||
|
self.isLoading = false
|
||||||
|
|
||||||
if let cachedContent = ContinueReadingManager.shared.getCachedHtml(for: chapterHref),
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
!cachedContent.isEmpty &&
|
withAnimation(.easeInOut(duration: 0.3)) {
|
||||||
!cachedContent.contains("undefined") &&
|
self.isHeaderVisible = false
|
||||||
cachedContent.count > 50 {
|
self.statusBarHidden = true
|
||||||
Logger.shared.log("Using cached HTML content for \(chapterHref)", type: "Debug")
|
self.setStatusBarHidden(true)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} 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
|
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) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
DropManager.shared.showDrop(
|
self.fetchContentWithRetries(attempts: attempts + 1, maxAttempts: maxAttempts, lastError: error)
|
||||||
title: "Error Loading Content",
|
|
||||||
subtitle: error.localizedDescription,
|
|
||||||
duration: 2.0,
|
|
||||||
icon: UIImage(systemName: "exclamationmark.triangle")
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.statusBar(hidden: statusBarHidden)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func stopAutoScroll() {
|
private func stopAutoScroll() {
|
||||||
|
|
@ -953,7 +963,7 @@ struct ReaderView: View {
|
||||||
|
|
||||||
UserDefaults.standard.set(roundedProgress, forKey: "readingProgress_\(chapterHref)")
|
UserDefaults.standard.set(roundedProgress, forKey: "readingProgress_\(chapterHref)")
|
||||||
|
|
||||||
var novelTitle = self.mediaTitle
|
let novelTitle = self.mediaTitle
|
||||||
var currentChapterNumber = 1
|
var currentChapterNumber = 1
|
||||||
var imageUrl = ""
|
var imageUrl = ""
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue