mirror of
https://github.com/cranci1/Sora.git
synced 2026-04-22 09:02:04 +00:00
Rewrote the entire novel system 😭
This commit is contained in:
parent
d0751c2ffd
commit
c722ec9b29
3 changed files with 180 additions and 42 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue