mirror of
https://github.com/cranci1/Sora.git
synced 2026-01-11 20:10:24 +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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = ""
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue