Restore the article scroll position when restarting NetNewsWire

This commit is contained in:
Maurice Parker 2021-09-13 20:42:58 -05:00
parent f44308c0c5
commit 2c801a388c
4 changed files with 91 additions and 19 deletions

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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