Rewrote the entire novel system 😭
Some checks are pending
Build and Release / Build IPA (push) Waiting to run
Build and Release / Build Mac Catalyst (push) Waiting to run

This commit is contained in:
cranci1 2025-07-13 10:43:15 +02:00
parent d0751c2ffd
commit c722ec9b29
3 changed files with 180 additions and 42 deletions

View file

@ -267,7 +267,7 @@ extension JSController {
Logger.shared.log("Attempting direct fetch from: \(url.absoluteString)", type: "Debug")
let task = URLSession.shared.dataTask(with: request) { data, response, error in
let task = URLSession.shared.dataTask(with: request) { [self] data, response, error in
if let error = error {
DispatchQueue.main.async {
Logger.shared.log("Direct fetch error: \(error.localizedDescription)", type: "Error")
@ -300,25 +300,50 @@ extension JSController {
let startIndex = contentRange.lowerBound
let endIndex = endRange.upperBound
content = String(htmlString[startIndex..<endIndex])
Logger.shared.log("Extracted content from <article> tag", type: "Debug")
} 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])
Logger.shared.log("Extracted content from chapter-content div", type: "Debug")
} 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])
Logger.shared.log("Extracted content from content div", type: "Debug")
} else if let contentRange = htmlString.range(of: "<div id=\"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])
Logger.shared.log("Extracted content from chapter-content id div", type: "Debug")
} else if let contentRange = htmlString.range(of: "<div class=\"chapter\"", 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])
Logger.shared.log("Extracted content from chapter div", type: "Debug")
} else if let contentRange = htmlString.range(of: "<main", options: .caseInsensitive),
let endRange = htmlString.range(of: "</main>", options: .caseInsensitive) {
let startIndex = contentRange.lowerBound
let endIndex = endRange.upperBound
content = String(htmlString[startIndex..<endIndex])
Logger.shared.log("Extracted content from <main> tag", type: "Debug")
} 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])
Logger.shared.log("Extracted content from <body> tag", type: "Debug")
} else {
content = htmlString
Logger.shared.log("Using full HTML content", type: "Debug")
}
content = cleanHTMLContent(content)
DispatchQueue.main.async {
Logger.shared.log("Direct fetch successful, content length: \(content.count)", type: "Debug")
completion(.success(content))
@ -327,4 +352,55 @@ extension JSController {
task.resume()
}
private func cleanHTMLContent(_ content: String) -> String {
var cleaned = content
cleaned = cleaned.replacingOccurrences(
of: "<script[^>]*>.*?</script>",
with: "",
options: [.regularExpression, .caseInsensitive]
)
cleaned = cleaned.replacingOccurrences(
of: "<style[^>]*>.*?</style>",
with: "",
options: [.regularExpression, .caseInsensitive]
)
cleaned = cleaned.replacingOccurrences(
of: "<nav[^>]*>.*?</nav>",
with: "",
options: [.regularExpression, .caseInsensitive]
)
cleaned = cleaned.replacingOccurrences(
of: "<header[^>]*>.*?</header>",
with: "",
options: [.regularExpression, .caseInsensitive]
)
cleaned = cleaned.replacingOccurrences(
of: "<footer[^>]*>.*?</footer>",
with: "",
options: [.regularExpression, .caseInsensitive]
)
let unwantedClasses = ["advertisement", "ad", "ads", "sidebar", "menu", "navigation", "nav", "header", "footer", "comments", "comment"]
for className in unwantedClasses {
cleaned = cleaned.replacingOccurrences(
of: "<div[^>]*class=\"[^\"]*\(className)[^\"]*\"[^>]*>.*?</div>",
with: "",
options: [.regularExpression, .caseInsensitive]
)
}
cleaned = cleaned.replacingOccurrences(
of: "\\s+",
with: " ",
options: .regularExpression
)
return cleaned.trimmingCharacters(in: .whitespacesAndNewlines)
}
}

View file

@ -1221,6 +1221,13 @@ struct MediaInfoView: View {
let maxIndex = max(0, groupedEpisodes().count - 1)
selectedSeason = min(savedSeason, maxIndex)
}
if let savedChapterStart = UserDefaults.standard.object(forKey: selectedChapterRangeKey) as? Int,
let savedChapterRange = generateChapterRanges().first(where: { $0.lowerBound == savedChapterStart }) {
selectedChapterRange = savedChapterRange
} else {
selectedChapterRange = generateChapterRanges().first ?? 0..<chapterChunkSize
}
}
private func generateRanges() -> [Range<Int>] {
@ -1267,6 +1274,41 @@ struct MediaInfoView: View {
}
private func playFirstUnwatchedEpisode() {
if module.metadata.novel ?? false {
guard !chapters.isEmpty else { return }
var firstUnreadChapter: [String: Any]? = nil
for chapter in chapters {
if let href = chapter["href"] as? String {
let progress = UserDefaults.standard.double(forKey: "readingProgress_\(href)")
if progress < 0.95 {
firstUnreadChapter = chapter
break
}
}
}
let chapterToRead = firstUnreadChapter ?? chapters[0]
if let href = chapterToRead["href"] as? String,
let title = chapterToRead["title"] as? String,
let number = chapterToRead["number"] as? Int {
UserDefaults.standard.set(true, forKey: "navigatingToReaderView")
ChapterNavigator.shared.currentChapter = (
moduleId: module.id,
href: href,
title: title,
chapters: chapters,
mediaTitle: self.title,
chapterNumber: number
)
Logger.shared.log("Navigating to chapter: \(title)", type: "Debug")
}
return
}
let indices = finishedAndUnfinishedIndices()
let finished = indices.finished
let unfinished = indices.unfinished
@ -1415,10 +1457,17 @@ struct MediaInfoView: View {
self.jsController.loadScript(jsContent)
let completion: (Any?, [EpisodeLink]) -> Void = { items, episodes in
if self.module.metadata.novel ?? true {
if self.module.metadata.novel ?? false {
self.processItemsResponse(items)
self.jsController.extractChapters(moduleId: self.module.id, href: self.href) { chapters in
DispatchQueue.main.async {
self.handleFetchDetailsResponse(items: chapters, episodes: episodes)
self.chapters = chapters
Logger.shared.log("fetchDetails: (novel) chapters count = \(self.chapters.count)", type: "Debug")
self.restoreSelectionState()
self.isLoading = false
self.isRefetching = false
}
}
} else {
@ -1442,22 +1491,11 @@ struct MediaInfoView: View {
private func handleFetchDetailsResponse(items: Any?, episodes: [EpisodeLink]) {
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 {
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
restoreSelectionState()
}
isLoading = false
isRefetching = false

View file

@ -54,8 +54,6 @@ struct ReaderView: View {
@Environment(\.dismiss) private var dismiss
@StateObject private var navigator = ChapterNavigator.shared
// Status bar control
@State private var statusBarHidden = false
private let fontOptions = [
@ -330,6 +328,9 @@ struct ReaderView: View {
self.setStatusBarHidden(true)
}
}
} else {
Logger.shared.log("No valid cached content found, fetching new content for \(self.chapterHref)", type: "Debug")
fetchContentWithRetries(attempts: 0, maxAttempts: 3)
}
} catch {
self.error = error
@ -378,6 +379,7 @@ struct ReaderView: View {
return
}
DispatchQueue.main.async {
self.htmlContent = content
self.isLoading = false
@ -388,6 +390,7 @@ struct ReaderView: View {
self.setStatusBarHidden(true)
}
}
}
if let cachedContent = ContinueReadingManager.shared.getCachedHtml(for: self.chapterHref),
cachedContent.isEmpty || cachedContent.contains("undefined") || cachedContent.count < 50 {
@ -1160,6 +1163,16 @@ struct HTMLView: UIViewRepresentable {
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
Logger.shared.log("WebView finished loading navigation", type: "Debug")
webView.evaluateJavaScript("document.body.innerText.length") { result, error in
if let textLength = result as? Int {
Logger.shared.log("WebView loaded content with text length: \(textLength)", type: "Debug")
} else {
Logger.shared.log("WebView error checking content length: \(error?.localizedDescription ?? "Unknown error")", type: "Error")
}
}
if let href = parent.chapterHref {
let savedPosition = UserDefaults.standard.double(forKey: "scrollPosition_\(href)")
if savedPosition > 0.01 {
@ -1177,6 +1190,14 @@ struct HTMLView: UIViewRepresentable {
startProgressTracking(webView: webView)
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
Logger.shared.log("WebView navigation failed: \(error.localizedDescription)", type: "Error")
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
Logger.shared.log("WebView provisional navigation failed: \(error.localizedDescription)", type: "Error")
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "scrollHandler", let webView = self.webView {
updateReadingProgress(webView: webView)
@ -1329,9 +1350,12 @@ struct HTMLView: UIViewRepresentable {
}
guard !htmlContent.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
Logger.shared.log("HTMLView: Empty HTML content, skipping update", type: "Warning")
return
}
Logger.shared.log("HTMLView: Updating with content length: \(htmlContent.count)", type: "Debug")
let contentChanged = coordinator.lastHtmlContent != htmlContent
let fontSizeChanged = coordinator.lastFontSize != fontSize
let fontFamilyChanged = coordinator.lastFontFamily != fontFamily