Restore the article scroll position when restarting NetNewsWire
This commit is contained in:
parent
f44308c0c5
commit
2c801a388c
|
@ -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
|
||||
|
|
|
@ -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? {
|
||||
|
@ -63,6 +68,11 @@ final class DetailWebView: WKWebView {
|
|||
}
|
||||
}
|
||||
|
||||
override func scrollWheel(with event: NSEvent) {
|
||||
super.scrollWheel(with: event)
|
||||
detailDelegate?.didScroll(self)
|
||||
}
|
||||
|
||||
private var inBigSurOffsetFix = false
|
||||
|
||||
private func bigSurOffsetFix() {
|
||||
|
|
|
@ -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,8 +61,18 @@ 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"
|
||||
|
@ -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,13 +271,13 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate {
|
|||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||
webView.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if let windowScrollY = windowScrollY {
|
||||
webView.evaluateJavaScript("window.scrollTo(0, \(windowScrollY));")
|
||||
self.windowScrollY = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WKUIDelegate
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue