test crash fix + layout fix

This commit is contained in:
cranci1 2025-07-01 18:11:16 +02:00
parent d707858ad7
commit 09b1d9b0b1
4 changed files with 167 additions and 77 deletions

View file

@ -9,7 +9,6 @@ import Foundation
import JavaScriptCore
extension JSController {
func fetchDetails(url: String, completion: @escaping ([MediaItem], [EpisodeLink]) -> Void) {
guard let url = URL(string: url) else {
completion([], [])
@ -94,41 +93,64 @@ extension JSController {
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
var hasLeftDetailsGroup = false
let detailsGroupQueue = DispatchQueue(label: "details.group")
let promiseValueDetails = extractDetailsFunction.call(withArguments: [url.absoluteString])
guard let promiseDetails = promiseValueDetails else {
Logger.shared.log("extractDetails did not return a Promise", type: "Error")
dispatchGroup.leave()
detailsGroupQueue.sync {
guard !hasLeftDetailsGroup else { return }
hasLeftDetailsGroup = true
dispatchGroup.leave()
}
completion([], [])
return
}
let thenBlockDetails: @convention(block) (JSValue) -> Void = { result in
if let jsonOfDetails = result.toString(),
let dataDetails = jsonOfDetails.data(using: .utf8) {
do {
if let array = try JSONSerialization.jsonObject(with: dataDetails, options: []) as? [[String: Any]] {
resultItems = array.map { item -> MediaItem in
MediaItem(
description: item["description"] as? String ?? "",
aliases: item["aliases"] as? String ?? "",
airdate: item["airdate"] as? String ?? ""
)
}
} else {
Logger.shared.log("Failed to parse JSON of extractDetails", type: "Error")
}
} catch {
Logger.shared.log("JSON parsing error of extract details: \(error)", type: "Error")
detailsGroupQueue.sync {
guard !hasLeftDetailsGroup else {
Logger.shared.log("extractDetails: thenBlock called but group already left", type: "Debug")
return
}
} else {
Logger.shared.log("Result is not a string of extractDetails", type: "Error")
hasLeftDetailsGroup = true
if let jsonOfDetails = result.toString(),
let dataDetails = jsonOfDetails.data(using: .utf8) {
do {
if let array = try JSONSerialization.jsonObject(with: dataDetails, options: []) as? [[String: Any]] {
resultItems = array.map { item -> MediaItem in
MediaItem(
description: item["description"] as? String ?? "",
aliases: item["aliases"] as? String ?? "",
airdate: item["airdate"] as? String ?? ""
)
}
} else {
Logger.shared.log("Failed to parse JSON of extractDetails", type: "Error")
}
} catch {
Logger.shared.log("JSON parsing error of extract details: \(error)", type: "Error")
}
} else {
Logger.shared.log("Result is not a string of extractDetails", type: "Error")
}
dispatchGroup.leave()
}
dispatchGroup.leave()
}
let catchBlockDetails: @convention(block) (JSValue) -> Void = { error in
Logger.shared.log("Promise rejected of extractDetails: \(String(describing: error.toString()))", type: "Error")
dispatchGroup.leave()
detailsGroupQueue.sync {
guard !hasLeftDetailsGroup else {
Logger.shared.log("extractDetails: catchBlock called but group already left", type: "Debug")
return
}
hasLeftDetailsGroup = true
Logger.shared.log("Promise rejected of extractDetails: \(String(describing: error.toString()))", type: "Error")
dispatchGroup.leave()
}
}
let thenFunctionDetails = JSValue(object: thenBlockDetails, in: context)
@ -140,50 +162,80 @@ extension JSController {
dispatchGroup.enter()
let promiseValueEpisodes = extractEpisodesFunction.call(withArguments: [url.absoluteString])
var hasLeftEpisodesGroup = false
let episodesGroupQueue = DispatchQueue(label: "episodes.group")
let timeoutWorkItem = DispatchWorkItem {
Logger.shared.log("Timeout for extractEpisodes", type: "Warning")
dispatchGroup.leave()
episodesGroupQueue.sync {
guard !hasLeftEpisodesGroup else {
Logger.shared.log("extractEpisodes: timeout called but group already left", type: "Debug")
return
}
hasLeftEpisodesGroup = true
dispatchGroup.leave()
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0, execute: timeoutWorkItem)
guard let promiseEpisodes = promiseValueEpisodes else {
Logger.shared.log("extractEpisodes did not return a Promise", type: "Error")
timeoutWorkItem.cancel()
dispatchGroup.leave()
episodesGroupQueue.sync {
guard !hasLeftEpisodesGroup else { return }
hasLeftEpisodesGroup = true
dispatchGroup.leave()
}
completion([], [])
return
}
let thenBlockEpisodes: @convention(block) (JSValue) -> Void = { result in
timeoutWorkItem.cancel()
if let jsonOfEpisodes = result.toString(),
let dataEpisodes = jsonOfEpisodes.data(using: .utf8) {
do {
if let array = try JSONSerialization.jsonObject(with: dataEpisodes, options: []) as? [[String: Any]] {
episodeLinks = array.map { item -> EpisodeLink in
EpisodeLink(
number: item["number"] as? Int ?? 0,
title: "",
href: item["href"] as? String ?? "",
duration: nil
)
}
} else {
Logger.shared.log("Failed to parse JSON of extractEpisodes", type: "Error")
}
} catch {
Logger.shared.log("JSON parsing error of extractEpisodes: \(error)", type: "Error")
episodesGroupQueue.sync {
guard !hasLeftEpisodesGroup else {
Logger.shared.log("extractEpisodes: thenBlock called but group already left", type: "Debug")
return
}
} else {
Logger.shared.log("Result is not a string of extractEpisodes", type: "Error")
hasLeftEpisodesGroup = true
if let jsonOfEpisodes = result.toString(),
let dataEpisodes = jsonOfEpisodes.data(using: .utf8) {
do {
if let array = try JSONSerialization.jsonObject(with: dataEpisodes, options: []) as? [[String: Any]] {
episodeLinks = array.map { item -> EpisodeLink in
EpisodeLink(
number: item["number"] as? Int ?? 0,
title: "",
href: item["href"] as? String ?? "",
duration: nil
)
}
} else {
Logger.shared.log("Failed to parse JSON of extractEpisodes", type: "Error")
}
} catch {
Logger.shared.log("JSON parsing error of extractEpisodes: \(error)", type: "Error")
}
} else {
Logger.shared.log("Result is not a string of extractEpisodes", type: "Error")
}
dispatchGroup.leave()
}
dispatchGroup.leave()
}
let catchBlockEpisodes: @convention(block) (JSValue) -> Void = { error in
timeoutWorkItem.cancel()
Logger.shared.log("Promise rejected of extractEpisodes: \(String(describing: error.toString()))", type: "Error")
dispatchGroup.leave()
episodesGroupQueue.sync {
guard !hasLeftEpisodesGroup else {
Logger.shared.log("extractEpisodes: catchBlock called but group already left", type: "Debug")
return
}
hasLeftEpisodesGroup = true
Logger.shared.log("Promise rejected of extractEpisodes: \(String(describing: error.toString()))", type: "Error")
dispatchGroup.leave()
}
}
let thenFunctionEpisodes = JSValue(object: thenBlockEpisodes, in: context)

View file

@ -59,30 +59,48 @@ extension JSController {
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")
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")
groupQueue.sync {
guard !hasLeftGroup else {
Logger.shared.log("extractChapters: thenBlock called but group already left", type: "Debug")
return
}
} else {
Logger.shared.log("extractChapters: could not parse result", type: "Error")
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")
}
} else {
Logger.shared.log("extractChapters: could not parse result", type: "Error")
}
group.leave()
}
group.leave()
}
let catchBlock: @convention(block) (JSValue) -> Void = { jsValue in
Logger.shared.log("extractChapters catchBlock: \(jsValue)", type: "Error")
group.leave()
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])
@ -182,24 +200,42 @@ extension JSController {
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")
if let text = jsValue.toString(), !text.isEmpty {
Logger.shared.log("extractText: successfully extracted text", type: "Debug")
extractedText = text
} else {
extractError = JSError.emptyContent
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()
}
group.leave()
}
let catchBlock: @convention(block) (JSValue) -> Void = { jsValue in
Logger.shared.log("extractText catchBlock: \(jsValue)", type: "Error")
if extractedText.isEmpty {
extractError = JSError.jsException(jsValue.toString() ?? "Unknown 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()
}
group.leave()
}
result.invokeMethod("then", withArguments: [thenBlock])
@ -277,8 +313,8 @@ extension JSController {
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
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
}
@ -317,4 +353,4 @@ extension JSController {
Logger.shared.log("Direct fetch successful, content length: \(content.count)", type: "Debug")
return content
}
}
}

View file

@ -341,6 +341,8 @@ struct SettingsViewGeneral: View {
.font(.caption)
.foregroundStyle(.gray)
.frame(maxWidth: .infinity, alignment: .center)
.padding(.top, -6)
.padding(.bottom, 8)
}
.environment(\.editMode, .constant(.active))
}

View file

@ -634,8 +634,8 @@
isa = PBXGroup;
children = (
134A387B2DE4B5B90041B687 /* Downloads */,
04536F702E04BA3B00A11248 /* JSController-Novel.swift */,
133D7C8B2D2BE2640075467E /* JSController.swift */,
04536F702E04BA3B00A11248 /* JSController-Novel.swift */,
132AF1202D99951700A0140B /* JSController-Streams.swift */,
132AF1222D9995C300A0140B /* JSController-Details.swift */,
132AF1242D9995F900A0140B /* JSController-Search.swift */,