diff --git a/Mac/MainWindow/Detail/DetailViewController.swift b/Mac/MainWindow/Detail/DetailViewController.swift index 250a04c3a..f8758a852 100644 --- a/Mac/MainWindow/Detail/DetailViewController.swift +++ b/Mac/MainWindow/Detail/DetailViewController.swift @@ -16,8 +16,8 @@ enum DetailState: Equatable { case noSelection case multipleSelection case loading - case article(Article) - case extracted(Article, ExtractedArticle) + case article(Article, CGFloat?) + case extracted(Article, ExtractedArticle, CGFloat?) } final class DetailViewController: NSViewController, WKUIDelegate { @@ -81,13 +81,18 @@ final class DetailViewController: NSViewController, WKUIDelegate { // MARK: - Navigation func focus() { - guard let window = currentWebViewController.webView.window else { return } window.makeFirstResponderUnlessDescendantIsFirstResponder(currentWebViewController.webView) } + // MARK: State Restoration + + func saveState(to state: inout [AnyHashable : Any]) { + currentWebViewController.saveState(to: &state) + } + } // MARK: - DetailWebViewControllerDelegate diff --git a/Mac/MainWindow/Detail/DetailWebView.swift b/Mac/MainWindow/Detail/DetailWebView.swift index a6e4a5c45..450468556 100644 --- a/Mac/MainWindow/Detail/DetailWebView.swift +++ b/Mac/MainWindow/Detail/DetailWebView.swift @@ -10,8 +10,13 @@ import AppKit import WebKit import RSCore +protocol DetailWebViewDelegate: AnyObject { + func didScroll(_ : DetailWebView) +} + final class DetailWebView: WKWebView { + weak var detailDelegate: DetailWebViewDelegate? weak var keyboardDelegate: KeyboardDelegate? override func accessibilityLabel() -> String? { @@ -62,6 +67,11 @@ final class DetailWebView: WKWebView { bigSurOffsetFix() } } + + override func scrollWheel(with event: NSEvent) { + super.scrollWheel(with: event) + detailDelegate?.didScroll(self) + } private var inBigSurOffsetFix = false diff --git a/Mac/MainWindow/Detail/DetailWebViewController.swift b/Mac/MainWindow/Detail/DetailWebViewController.swift index 08d559032..a3d9281c6 100644 --- a/Mac/MainWindow/Detail/DetailWebViewController.swift +++ b/Mac/MainWindow/Detail/DetailWebViewController.swift @@ -24,6 +24,12 @@ final class DetailWebViewController: NSViewController { var state: DetailState = .noSelection { didSet { if state != oldValue { + switch state { + case .article(_, let scrollY), .extracted(_, _, let scrollY): + windowScrollY = scrollY + default: + break + } reloadHTML() } } @@ -31,9 +37,9 @@ final class DetailWebViewController: NSViewController { var article: Article? { switch state { - case .article(let article): + case .article(let article, _): return article - case .extracted(let article, _): + case .extracted(let article, _, _): return article default: return nil @@ -55,9 +61,19 @@ final class DetailWebViewController: NSViewController { private let detailIconSchemeHandler = DetailIconSchemeHandler() private var waitingForFirstReload = false - private var windowScrollY: CGFloat? private let keyboardDelegate = DetailKeyboardDelegate() - + private let scrollPositionQueue = CoalescingQueue(name: "Article Scroll Position", interval: 0.3, maxInterval: 0.3) + private var windowScrollY: CGFloat? + + private var isShowingExtractedArticle: Bool { + switch state { + case .extracted(_, _, _): + return true + default: + return false + } + } + private struct MessageName { static let mouseDidEnter = "mouseDidEnter" static let mouseDidExit = "mouseDidExit" @@ -81,6 +97,7 @@ final class DetailWebViewController: NSViewController { webView = DetailWebView(frame: NSRect.zero, configuration: configuration) webView.uiDelegate = self webView.navigationDelegate = self + webView.detailDelegate = self webView.keyboardDelegate = keyboardDelegate webView.translatesAutoresizingMaskIntoConstraints = false if let userAgent = UserAgent.fromInfoPlist() { @@ -177,6 +194,31 @@ final class DetailWebViewController: NSViewController { override func scrollPageUp(_ sender: Any?) { webView.scrollPageUp(sender) } + + // MARK: State Restoration + + func saveState(to state: inout [AnyHashable : Any]) { + state[UserInfoKey.isShowingExtractedArticle] = isShowingExtractedArticle + state[UserInfoKey.articleWindowScrollY] = windowScrollY + } + +} + +// MARK: DetailWebViewDelegate + +extension DetailWebViewController: DetailWebViewDelegate { + + func didScroll(_: DetailWebView) { + + scrollPositionQueue.add(self, #selector(scrollPositionDidChange)) + } + + @objc func scrollPositionDidChange() { + fetchScrollInfo() { scrollInfo in + self.windowScrollY = scrollInfo?.offsetY + } + } + } // MARK: - WKScriptMessageHandler @@ -229,11 +271,11 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate { DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { webView.isHidden = false } - } - - if let windowScrollY = windowScrollY { - webView.evaluateJavaScript("window.scrollTo(0, \(windowScrollY));") - self.windowScrollY = nil + } else { + if let windowScrollY = windowScrollY { + webView.evaluateJavaScript("window.scrollTo(0, \(windowScrollY));") + self.windowScrollY = nil + } } } @@ -281,10 +323,10 @@ private extension DetailWebViewController { rendering = ArticleRenderer.multipleSelectionHTML(theme: theme) case .loading: rendering = ArticleRenderer.loadingHTML(theme: theme) - case .article(let article): + case .article(let article, _): detailIconSchemeHandler.currentArticle = article rendering = ArticleRenderer.articleHTML(article: article, theme: theme) - case .extracted(let article, let extractedArticle): + case .extracted(let article, let extractedArticle, _): detailIconSchemeHandler.currentArticle = article rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, theme: theme) } diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index aec3369c7..dcfbc733c 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -56,6 +56,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { } } private var searchSmartFeed: SmartFeed? = nil + private var restoreArticleWindowScrollY: CGFloat? // MARK: - NSWindowController @@ -410,20 +411,20 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { articleExtractor?.cancel() articleExtractor = nil isShowingExtractedArticle = false - detailViewController?.setState(DetailState.article(article), mode: timelineSourceMode) + detailViewController?.setState(DetailState.article(article, nil), mode: timelineSourceMode) return } guard !isShowingExtractedArticle else { isShowingExtractedArticle = false - detailViewController?.setState(DetailState.article(article), mode: timelineSourceMode) + detailViewController?.setState(DetailState.article(article, nil), mode: timelineSourceMode) return } if let articleExtractor = articleExtractor, let extractedArticle = articleExtractor.article { if currentLink == articleExtractor.articleLink { isShowingExtractedArticle = true - let detailState = DetailState.extracted(article, extractedArticle) + let detailState = DetailState.extracted(article, extractedArticle, nil) detailViewController?.setState(detailState, mode: timelineSourceMode) } } else { @@ -619,7 +620,8 @@ extension MainWindowController: TimelineContainerViewControllerDelegate { detailState = .loading startArticleExtractorForCurrentLink() } else { - detailState = .article(articles.first!) + detailState = .article(articles.first!, restoreArticleWindowScrollY) + restoreArticleWindowScrollY = nil } } else { detailState = .multipleSelection @@ -719,7 +721,8 @@ extension MainWindowController: ArticleExtractorDelegate { func articleExtractionDidComplete(extractedArticle: ExtractedArticle) { if let article = oneSelectedArticle, articleExtractor?.state != .cancelled { isShowingExtractedArticle = true - let detailState = DetailState.extracted(article, extractedArticle) + let detailState = DetailState.extracted(article, extractedArticle, restoreArticleWindowScrollY) + restoreArticleWindowScrollY = nil detailViewController?.setState(detailState, mode: timelineSourceMode) makeToolbarValidate() } @@ -1043,6 +1046,7 @@ private extension MainWindowController { saveSplitViewState(to: &state) sidebarViewController?.saveState(to: &state) timelineContainerViewController?.saveState(to: &state) + detailViewController?.saveState(to: &state) return state } @@ -1051,8 +1055,19 @@ private extension MainWindowController { window?.toggleFullScreen(self) } restoreSplitViewState(from: state) + sidebarViewController?.restoreState(from: state) + + let articleWindowScrollY = state[UserInfoKey.articleWindowScrollY] as? CGFloat + restoreArticleWindowScrollY = articleWindowScrollY timelineContainerViewController?.restoreState(from: state) + + let isShowingExtractedArticle = state[UserInfoKey.isShowingExtractedArticle] as? Bool ?? false + if isShowingExtractedArticle { + restoreArticleWindowScrollY = articleWindowScrollY + startArticleExtractorForCurrentLink() + } + } // MARK: - Command Validation