mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge branch 'ios-candidate' of https://github.com/Ranchero-Software/NetNewsWire into ios-candidate
This commit is contained in:
@@ -272,6 +272,11 @@ public final class AccountManager: UnreadCountProvider {
|
||||
var allFetchedArticles = Set<Article>()
|
||||
let numberOfAccounts = activeAccounts.count
|
||||
var accountsReporting = 0
|
||||
|
||||
guard numberOfAccounts > 0 else {
|
||||
completion(.success(allFetchedArticles))
|
||||
return
|
||||
}
|
||||
|
||||
for account in activeAccounts {
|
||||
account.fetchArticlesAsync(fetchType) { (articleSetResult) in
|
||||
|
||||
@@ -22,12 +22,12 @@ function stripStyles() {
|
||||
document.getElementsByTagName("body")[0].querySelectorAll("[style]").forEach(element => stripStylesFromElement(element, ["color", "background", "font"]));
|
||||
}
|
||||
|
||||
// Convert all Feedbin proxy images to be used as src, otherwise change image locations to be absolute
|
||||
// Convert all Feedbin proxy images to be used as src, otherwise change image locations to be absolute if not already
|
||||
function convertImgSrc() {
|
||||
document.querySelectorAll("img").forEach(element => {
|
||||
if (element.hasAttribute("data-canonical-src")) {
|
||||
element.src = element.getAttribute("data-canonical-src")
|
||||
} else {
|
||||
} else if (!element.src.match(/^[a-z]+\:\/\//i)) {
|
||||
element.src = new URL(element.src, document.baseURI).href;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -14,6 +14,11 @@ import SafariServices
|
||||
|
||||
class ArticleViewController: UIViewController {
|
||||
|
||||
typealias State = (extractedArticle: ExtractedArticle?,
|
||||
isShowingExtractedArticle: Bool,
|
||||
articleExtractorButtonState: ArticleExtractorButtonState,
|
||||
windowScrollY: Int)
|
||||
|
||||
@IBOutlet private weak var nextUnreadBarButtonItem: UIBarButtonItem!
|
||||
@IBOutlet private weak var prevArticleBarButtonItem: UIBarButtonItem!
|
||||
@IBOutlet private weak var nextArticleBarButtonItem: UIBarButtonItem!
|
||||
@@ -49,7 +54,16 @@ class ArticleViewController: UIViewController {
|
||||
updateUI()
|
||||
}
|
||||
}
|
||||
var restoreWindowScrollY = 0
|
||||
|
||||
var currentState: State? {
|
||||
guard let controller = currentWebViewController else { return nil}
|
||||
return State(extractedArticle: controller.extractedArticle,
|
||||
isShowingExtractedArticle: controller.isShowingExtractedArticle,
|
||||
articleExtractorButtonState: controller.articleExtractorButtonState,
|
||||
windowScrollY: controller.windowScrollY)
|
||||
}
|
||||
|
||||
var restoreState: State?
|
||||
|
||||
private let keyboardManager = KeyboardManager(type: .detail)
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
@@ -89,7 +103,12 @@ class ArticleViewController: UIViewController {
|
||||
])
|
||||
|
||||
let controller = createWebViewController(article)
|
||||
controller.restoreWindowScrollY = restoreWindowScrollY
|
||||
if let state = restoreState {
|
||||
controller.extractedArticle = state.extractedArticle
|
||||
controller.isShowingExtractedArticle = state.isShowingExtractedArticle
|
||||
controller.articleExtractorButtonState = state.articleExtractorButtonState
|
||||
controller.windowScrollY = state.windowScrollY
|
||||
}
|
||||
articleExtractorButton.buttonState = controller.articleExtractorButtonState
|
||||
pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil)
|
||||
|
||||
@@ -239,18 +258,16 @@ class ArticleViewController: UIViewController {
|
||||
currentWebViewController?.fullReload()
|
||||
}
|
||||
|
||||
func stopArticleExtractorIfProcessing() {
|
||||
currentWebViewController?.stopArticleExtractorIfProcessing()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: WebViewControllerDelegate
|
||||
|
||||
extension ArticleViewController: WebViewControllerDelegate {
|
||||
|
||||
func webViewController(_ webViewController: WebViewController, restoreWindowScrollYDidUpdate restoreWindowScrollY: Int) {
|
||||
if webViewController === currentWebViewController {
|
||||
self.restoreWindowScrollY = restoreWindowScrollY
|
||||
}
|
||||
}
|
||||
|
||||
func webViewController(_ webViewController: WebViewController, articleExtractorButtonStateDidUpdate buttonState: ArticleExtractorButtonState) {
|
||||
if webViewController === currentWebViewController {
|
||||
articleExtractorButton.buttonState = buttonState
|
||||
@@ -290,7 +307,7 @@ extension ArticleViewController: UIPageViewControllerDelegate {
|
||||
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
|
||||
guard finished, completed else { return }
|
||||
guard let article = currentWebViewController?.article else { return }
|
||||
coordinator.selectArticle(article)
|
||||
coordinator.selectArticle(article, animations: [.select, .scroll, .navigation])
|
||||
articleExtractorButton.buttonState = currentWebViewController?.articleExtractorButtonState ?? .off
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import Articles
|
||||
import SafariServices
|
||||
|
||||
protocol WebViewControllerDelegate: class {
|
||||
func webViewController(_: WebViewController, restoreWindowScrollYDidUpdate: Int)
|
||||
func webViewController(_: WebViewController, articleExtractorButtonStateDidUpdate: ArticleExtractorButtonState)
|
||||
}
|
||||
|
||||
@@ -39,8 +38,12 @@ class WebViewController: UIViewController {
|
||||
private var clickedImageCompletion: (() -> Void)?
|
||||
|
||||
private var articleExtractor: ArticleExtractor? = nil
|
||||
private var extractedArticle: ExtractedArticle?
|
||||
private var isShowingExtractedArticle = false {
|
||||
var extractedArticle: ExtractedArticle? {
|
||||
didSet {
|
||||
windowScrollY = 0
|
||||
}
|
||||
}
|
||||
var isShowingExtractedArticle = false {
|
||||
didSet {
|
||||
if isShowingExtractedArticle != oldValue {
|
||||
reloadHTML()
|
||||
@@ -64,18 +67,14 @@ class WebViewController: UIViewController {
|
||||
startArticleExtractor()
|
||||
}
|
||||
if article != oldValue {
|
||||
restoreWindowScrollY = 0
|
||||
windowScrollY = 0
|
||||
reloadHTML()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let scrollPositionQueue = CoalescingQueue(name: "Article Scroll Position", interval: 0.3, maxInterval: 1.0)
|
||||
var restoreWindowScrollY = 0 {
|
||||
didSet {
|
||||
delegate?.webViewController(self, restoreWindowScrollYDidUpdate: restoreWindowScrollY)
|
||||
}
|
||||
}
|
||||
var windowScrollY = 0
|
||||
|
||||
deinit {
|
||||
if webView != nil {
|
||||
@@ -122,8 +121,6 @@ class WebViewController: UIViewController {
|
||||
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked)
|
||||
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown)
|
||||
|
||||
// Even though page.html should be loaded into this webview, we have to do it again
|
||||
// to work around this bug: http://www.openradar.me/22855188
|
||||
self.reloadHTML()
|
||||
|
||||
self.view.setNeedsLayout()
|
||||
@@ -247,6 +244,12 @@ class WebViewController: UIViewController {
|
||||
|
||||
}
|
||||
|
||||
func stopArticleExtractorIfProcessing() {
|
||||
if articleExtractor?.state == .processing {
|
||||
stopArticleExtractor()
|
||||
}
|
||||
}
|
||||
|
||||
func showActivityDialog(popOverBarButtonItem: UIBarButtonItem? = nil) {
|
||||
guard let preferredLink = article?.preferredLink, let url = URL(string: preferredLink) else {
|
||||
return
|
||||
@@ -423,7 +426,7 @@ extension WebViewController: UIScrollViewDelegate {
|
||||
|
||||
@objc func scrollPositionDidChange() {
|
||||
webView?.evaluateJavaScript("window.scrollY") { (scrollY, _) in
|
||||
self.restoreWindowScrollY = scrollY as? Int ?? 0
|
||||
self.windowScrollY = scrollY as? Int ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,10 +484,10 @@ private extension WebViewController {
|
||||
var render = "error();"
|
||||
if let data = try? encoder.encode(templateData) {
|
||||
let json = String(data: data, encoding: .utf8)!
|
||||
render = "render(\(json), \(restoreWindowScrollY));"
|
||||
render = "render(\(json), \(windowScrollY));"
|
||||
}
|
||||
|
||||
restoreWindowScrollY = 0
|
||||
windowScrollY = 0
|
||||
|
||||
webView.scrollView.setZoomScale(1.0, animated: false)
|
||||
webView.evaluateJavaScript(render)
|
||||
|
||||
@@ -14,17 +14,20 @@ import WebKit
|
||||
class WebViewProvider: NSObject, WKNavigationDelegate {
|
||||
|
||||
let articleIconSchemeHandler: ArticleIconSchemeHandler
|
||||
let viewController: UIViewController
|
||||
|
||||
private let minimumQueueDepth = 3
|
||||
private let maximumQueueDepth = 6
|
||||
private var queue: [WKWebView] = []
|
||||
private var queue = UIView()
|
||||
|
||||
private var waitingForFirstLoad = true
|
||||
private var waitingCompletionHandler: ((WKWebView) -> ())?
|
||||
|
||||
init(coordinator: SceneCoordinator) {
|
||||
init(coordinator: SceneCoordinator, viewController: UIViewController) {
|
||||
articleIconSchemeHandler = ArticleIconSchemeHandler(coordinator: coordinator)
|
||||
self.viewController = viewController
|
||||
super.init()
|
||||
self.viewController.view.insertSubview(queue, at: 0)
|
||||
replenishQueueIfNeeded()
|
||||
}
|
||||
|
||||
@@ -37,12 +40,12 @@ class WebViewProvider: NSObject, WKNavigationDelegate {
|
||||
}
|
||||
|
||||
func enqueueWebView(_ webView: WKWebView) {
|
||||
guard queue.count < maximumQueueDepth else {
|
||||
guard queue.subviews.count < maximumQueueDepth else {
|
||||
return
|
||||
}
|
||||
|
||||
webView.navigationDelegate = self
|
||||
queue.insert(webView, at: 0)
|
||||
queue.insertSubview(webView, at: 0)
|
||||
|
||||
webView.loadFileURL(ArticleRenderer.page.url, allowingReadAccessTo: ArticleRenderer.page.baseURL)
|
||||
|
||||
@@ -63,7 +66,7 @@ class WebViewProvider: NSObject, WKNavigationDelegate {
|
||||
// MARK: Private
|
||||
|
||||
private func replenishQueueIfNeeded() {
|
||||
while queue.count < minimumQueueDepth {
|
||||
while queue.subviews.count < minimumQueueDepth {
|
||||
let preferences = WKPreferences()
|
||||
preferences.javaScriptCanOpenWindowsAutomatically = false
|
||||
preferences.javaScriptEnabled = true
|
||||
@@ -81,7 +84,8 @@ class WebViewProvider: NSObject, WKNavigationDelegate {
|
||||
}
|
||||
|
||||
private func completeRequest(completion: @escaping (WKWebView) -> ()) {
|
||||
if let webView = queue.popLast() {
|
||||
if let webView = queue.subviews.last as? WKWebView {
|
||||
webView.removeFromSuperview()
|
||||
webView.navigationDelegate = nil
|
||||
replenishQueueIfNeeded()
|
||||
completion(webView)
|
||||
|
||||
@@ -30,7 +30,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
return rootSplitViewController.undoManager
|
||||
}
|
||||
|
||||
lazy var webViewProvider = WebViewProvider(coordinator: self)
|
||||
lazy var webViewProvider = WebViewProvider(coordinator: self, viewController: rootSplitViewController)
|
||||
|
||||
private var panelMode: PanelMode = .unset
|
||||
|
||||
@@ -133,6 +133,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
return treeController.rootNode
|
||||
}
|
||||
|
||||
// At some point we should refactor the current Feed IndexPath out and only use the timeline feed
|
||||
private(set) var currentFeedIndexPath: IndexPath?
|
||||
|
||||
var timelineIconImage: IconImage? {
|
||||
@@ -445,6 +446,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
}
|
||||
|
||||
@objc func unreadCountDidChange(_ note: Notification) {
|
||||
// We will handle the filtering of unread feeds in unreadCountDidInitialize after they have all be calculated
|
||||
guard AccountManager.shared.isUnreadCountsInitialized else {
|
||||
return
|
||||
}
|
||||
|
||||
// If we are filtering reads, the new unread count is greater than 1, and the feed isn't shown then continue
|
||||
guard let feed = note.object as? Feed, isReadFeedsFiltered, feed.unreadCount > 0, !shadowTableContains(feed) else {
|
||||
return
|
||||
@@ -805,6 +811,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
|
||||
func endSearching() {
|
||||
if let ip = currentFeedIndexPath, let node = nodeFor(ip), let feed = node.representedObject as? Feed {
|
||||
emptyTheTimeline()
|
||||
timelineFeed = feed
|
||||
masterTimelineViewController?.reinitializeArticles(resetScroll: true)
|
||||
replaceArticles(with: savedSearchArticles!, animated: true)
|
||||
@@ -905,7 +912,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
|
||||
activityManager.selectingNextUnread()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if self.isSearching {
|
||||
self.masterTimelineViewController?.hideSearch()
|
||||
}
|
||||
|
||||
selectNextUnreadFeed() {
|
||||
if self.selectNextUnreadArticleInTimeline() {
|
||||
self.activityManager.selectingNextUnread()
|
||||
@@ -1251,14 +1262,45 @@ private extension SceneCoordinator {
|
||||
|
||||
func rebuildBackingStores(initialLoad: Bool = false, updateExpandedNodes: (() -> Void)? = nil) {
|
||||
if !animatingChanges && !BatchUpdate.shared.isPerforming {
|
||||
|
||||
addCurrentFeedToFilterExeptionsIfNecessary()
|
||||
treeController.rebuild()
|
||||
treeControllerDelegate.resetFilterExceptions()
|
||||
|
||||
updateExpandedNodes?()
|
||||
rebuildShadowTable()
|
||||
masterFeedViewController.reloadFeeds(initialLoad: initialLoad)
|
||||
clearTimelineIfNoLongerAvailable()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func addCurrentFeedToFilterExeptionsIfNecessary() {
|
||||
if isReadFeedsFiltered, let feedID = timelineFeed?.feedID {
|
||||
if timelineFeed is SmartFeed {
|
||||
treeControllerDelegate.addFilterException(feedID)
|
||||
} else if let folderFeed = timelineFeed as? Folder {
|
||||
if folderFeed.account?.existingFolder(withID: folderFeed.folderID) != nil {
|
||||
treeControllerDelegate.addFilterException(feedID)
|
||||
}
|
||||
} else if let webFeed = timelineFeed as? WebFeed {
|
||||
if webFeed.account?.existingWebFeed(withWebFeedID: webFeed.webFeedID) != nil {
|
||||
treeControllerDelegate.addFilterException(feedID)
|
||||
addParentFolderToFilterExceptions(webFeed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addParentFolderToFilterExceptions(_ feed: Feed) {
|
||||
guard let node = treeController.rootNode.descendantNodeRepresentingObject(feed as AnyObject),
|
||||
let folder = node.parent?.representedObject as? Folder,
|
||||
let folderFeedID = folder.feedID else {
|
||||
return
|
||||
}
|
||||
|
||||
treeControllerDelegate.addFilterException(folderFeedID)
|
||||
}
|
||||
|
||||
func rebuildShadowTable() {
|
||||
shadowTable = [[Node]]()
|
||||
|
||||
@@ -1281,6 +1323,11 @@ private extension SceneCoordinator {
|
||||
shadowTable.append(result)
|
||||
|
||||
}
|
||||
|
||||
// If we have a current Feed IndexPath it is no longer valid and needs reset.
|
||||
if currentFeedIndexPath != nil {
|
||||
currentFeedIndexPath = indexPathFor(timelineFeed as AnyObject)
|
||||
}
|
||||
}
|
||||
|
||||
func shadowTableContains(_ feed: Feed) -> Bool {
|
||||
@@ -1743,14 +1790,14 @@ private extension SceneCoordinator {
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func installArticleController(restoreWindowScrollY: Int = 0, animated: Bool) -> ArticleViewController {
|
||||
func installArticleController(state: ArticleViewController.State? = nil, animated: Bool) -> ArticleViewController {
|
||||
|
||||
isArticleViewControllerPending = true
|
||||
|
||||
let articleController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self)
|
||||
articleController.coordinator = self
|
||||
articleController.article = currentArticle
|
||||
articleController.restoreWindowScrollY = restoreWindowScrollY
|
||||
articleController.restoreState = state
|
||||
|
||||
if let subSplit = subSplitViewController {
|
||||
let controller = addNavControllerIfNecessary(articleController, showButton: false)
|
||||
@@ -1813,12 +1860,12 @@ private extension SceneCoordinator {
|
||||
}
|
||||
|
||||
func configureThreePanelMode() {
|
||||
let articleRestoreWindowScrollY = articleViewController?.restoreWindowScrollY ?? 0
|
||||
articleViewController?.stopArticleExtractorIfProcessing()
|
||||
let articleViewControllerState = articleViewController?.currentState
|
||||
defer {
|
||||
masterNavigationController.viewControllers = [masterFeedViewController]
|
||||
}
|
||||
|
||||
|
||||
if rootSplitViewController.viewControllers.last is InteractiveNavigationController {
|
||||
_ = rootSplitViewController.viewControllers.popLast()
|
||||
}
|
||||
@@ -1828,14 +1875,15 @@ private extension SceneCoordinator {
|
||||
masterTimelineViewController?.navigationItem.leftBarButtonItem = rootSplitViewController.displayModeButtonItem
|
||||
masterTimelineViewController?.navigationItem.leftItemsSupplementBackButton = true
|
||||
|
||||
installArticleController(restoreWindowScrollY: articleRestoreWindowScrollY, animated: false)
|
||||
installArticleController(state: articleViewControllerState, animated: false)
|
||||
|
||||
masterFeedViewController.restoreSelectionIfNecessary(adjustScroll: true)
|
||||
masterTimelineViewController!.restoreSelectionIfNecessary(adjustScroll: false)
|
||||
}
|
||||
|
||||
func configureStandardPanelMode() {
|
||||
let articleRestoreWindowScrollY = articleViewController?.restoreWindowScrollY ?? 0
|
||||
articleViewController?.stopArticleExtractorIfProcessing()
|
||||
let articleViewControllerState = articleViewController?.currentState
|
||||
rootSplitViewController.preferredPrimaryColumnWidthFraction = UISplitViewController.automaticDimension
|
||||
|
||||
// Set the is Pending flags early to prevent the navigation controller delegate from thinking that we
|
||||
@@ -1855,7 +1903,7 @@ private extension SceneCoordinator {
|
||||
masterNavigationController.pushViewController(masterTimelineViewController!, animated: false)
|
||||
}
|
||||
|
||||
installArticleController(restoreWindowScrollY: articleRestoreWindowScrollY, animated: false)
|
||||
installArticleController(state: articleViewControllerState, animated: false)
|
||||
}
|
||||
|
||||
// MARK: NSUserActivity
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
// High Level Settings common to both the iOS application and any extensions we bundle with it
|
||||
MARKETING_VERSION = 5.0
|
||||
CURRENT_PROJECT_VERSION = 31
|
||||
CURRENT_PROJECT_VERSION = 33
|
||||
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
|
||||
|
||||
Reference in New Issue
Block a user