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()
}
Logger.shared.log("fetchDetails: (episodes) episodes count = \(episodes.count)", type: "Debug")
episodeLinks = episodes
restoreSelectionState()
isLoading = false
isRefetching = false

View file

@ -52,10 +52,8 @@ struct ReaderView: View {
@State private var readingProgress: Double = 0.0
@State private var lastProgressUpdate: Date = Date()
@Environment(\.dismiss) private var dismiss
@StateObject private var navigator = ChapterNavigator.shared
// Status bar control
@StateObject private var navigator = ChapterNavigator.shared
@State private var statusBarHidden = false
private let fontOptions = [
@ -125,7 +123,7 @@ struct ReaderView: View {
}
}
var body: some View {
ZStack(alignment: .bottom) {
@ -161,7 +159,7 @@ struct ReaderView: View {
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
HTMLView(
htmlContent: htmlContent,
fontSize: fontSize,
@ -208,7 +206,7 @@ struct ReaderView: View {
.zIndex(1)
if isHeaderVisible {
footerView
footerView
.transition(.move(edge: .bottom))
.zIndex(2)
}
@ -268,7 +266,7 @@ struct ReaderView: View {
mediaTitle: next.mediaTitle,
chapterNumber: next.chapterNumber
)
let hostingController = UIHostingController(rootView: nextReader)
hostingController.modalPresentationStyle = .fullScreen
@ -280,8 +278,8 @@ struct ReaderView: View {
} else {
if !htmlContent.isEmpty {
let validHtmlContent = (!htmlContent.isEmpty &&
!htmlContent.contains("undefined") &&
htmlContent.count > 50) ? htmlContent : nil
!htmlContent.contains("undefined") &&
htmlContent.count > 50) ? htmlContent : nil
if validHtmlContent == nil {
Logger.shared.log("Not caching HTML content on disappear as it appears invalid", type: "Warning")
@ -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,14 +379,16 @@ struct ReaderView: View {
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)
DispatchQueue.main.async {
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)
}
}
}
@ -448,7 +451,7 @@ struct ReaderView: View {
.padding(.trailing, 100)
Spacer()
Color.clear
.frame(width: 44, height: 44)
.padding(.trailing)
@ -463,7 +466,7 @@ struct ReaderView: View {
isSettingsExpanded = false
}
}
HStack {
Spacer()
Button(action: {
@ -935,8 +938,8 @@ struct ReaderView: View {
Logger.shared.log("Saving continue reading item: title=\(novelTitle), chapter=\(chapterTitle), number=\(currentChapterNumber), href=\(chapterHref), progress=\(progress), imageUrl=\(imageUrl)", type: "Debug")
let validHtmlContent = (!htmlContent.isEmpty &&
!htmlContent.contains("undefined") &&
htmlContent.count > 50) ? htmlContent : nil
!htmlContent.contains("undefined") &&
htmlContent.count > 50) ? htmlContent : nil
if validHtmlContent == nil && !htmlContent.isEmpty {
Logger.shared.log("Not caching HTML content as it appears invalid", type: "Warning")
@ -1008,8 +1011,8 @@ struct ReaderView: View {
Logger.shared.log("Updating reading progress: \(roundedProgress) for \(chapterHref), title: \(novelTitle), image: \(imageUrl)", type: "Debug")
let validHtmlContent = (!htmlContent.isEmpty &&
!htmlContent.contains("undefined") &&
htmlContent.count > 50) ? htmlContent : nil
!htmlContent.contains("undefined") &&
htmlContent.count > 50) ? htmlContent : nil
if validHtmlContent == nil && !htmlContent.isEmpty {
Logger.shared.log("Not caching HTML content as it appears invalid", type: "Warning")
@ -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)
@ -1295,7 +1316,7 @@ struct HTMLView: UIViewRepresentable {
}
}
}
func makeUIView(context: Context) -> WKWebView {
let webView = WKWebView()
webView.backgroundColor = .clear
@ -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
@ -1342,7 +1366,7 @@ struct HTMLView: UIViewRepresentable {
let colorChanged = coordinator.lastColorPreset != colorPreset.name
if contentChanged || fontSizeChanged || fontFamilyChanged || fontWeightChanged ||
alignmentChanged || lineSpacingChanged || marginChanged || colorChanged {
alignmentChanged || lineSpacingChanged || marginChanged || colorChanged {
let htmlTemplate = """
<!DOCTYPE html>
<html>