From 3a8ec93644638a235c12875c1b39f7627ab715b6 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 24 Sep 2019 03:38:17 -0500 Subject: [PATCH 1/9] Remove unnecessary extractor specific errors --- Shared/Article Extractor/ArticleExtractor.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Shared/Article Extractor/ArticleExtractor.swift b/Shared/Article Extractor/ArticleExtractor.swift index ea1d64c67..7390cf94e 100644 --- a/Shared/Article Extractor/ArticleExtractor.swift +++ b/Shared/Article Extractor/ArticleExtractor.swift @@ -21,12 +21,6 @@ protocol ArticleExtractorDelegate { func articleExtractionDidComplete(extractedArticle: ExtractedArticle) } -enum ArticleExtractorError: Error { - case UnableToParseHTML - case MissingURL - case UnableToLoadURL -} - class ArticleExtractor { private var dataTask: URLSessionDataTask? = nil @@ -75,7 +69,7 @@ class ArticleExtractor { guard let data = data else { self.state = .failedToParse DispatchQueue.main.async { - self.delegate?.articleExtractionDidFail(with: ArticleExtractorError.UnableToLoadURL) + self.delegate?.articleExtractionDidFail(with: URLError(.cannotDecodeContentData)) } return } From 394618a687cb2cb08849aae14f0d8b19967e661c Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 24 Sep 2019 04:29:15 -0500 Subject: [PATCH 2/9] Rename DetailViewController to ArticleViewController to fix name collision --- NetNewsWire.xcodeproj/project.pbxproj | 22 ++++---- iOS/AppDelegate.swift | 2 +- .../ArticleViewController.swift} | 16 +++--- ...rticleViewControllerWebViewProvider.swift} | 6 +-- iOS/Base.lproj/Main.storyboard | 4 +- iOS/SceneCoordinator.swift | 50 +++++++++---------- 6 files changed, 50 insertions(+), 50 deletions(-) rename iOS/{Detail/DetailViewController.swift => Article/ArticleViewController.swift} (95%) rename iOS/{Detail/DetailViewControllerWebViewProvider.swift => Article/ArticleViewControllerWebViewProvider.swift} (92%) diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 3752177c8..8baf5d9c1 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -79,7 +79,7 @@ 5170743A232AABFC00A461A3 /* FlattenedAccountFolderPickerData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452812265093600C03939 /* FlattenedAccountFolderPickerData.swift */; }; 517630042336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; }; 517630052336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; }; - 517630232336657E00E15FFF /* DetailViewControllerWebViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517630222336657E00E15FFF /* DetailViewControllerWebViewProvider.swift */; }; + 517630232336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517630222336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift */; }; 5183CCD0226E1E880010922C /* NonIntrinsicLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */; }; 5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */; }; 5183CCDD226F1F5C0010922C /* NavigationProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCDC226F1F5C0010922C /* NavigationProgressView.swift */; }; @@ -142,7 +142,7 @@ 51C452792265091600C03939 /* MasterTimelineTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452722265091600C03939 /* MasterTimelineTableViewCell.swift */; }; 51C4527B2265091600C03939 /* MasterUnreadIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452742265091600C03939 /* MasterUnreadIndicatorView.swift */; }; 51C4527C2265091600C03939 /* MasterTimelineDefaultCellLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452752265091600C03939 /* MasterTimelineDefaultCellLayout.swift */; }; - 51C4527F2265092C00C03939 /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4527E2265092C00C03939 /* DetailViewController.swift */; }; + 51C4527F2265092C00C03939 /* ArticleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4527E2265092C00C03939 /* ArticleViewController.swift */; }; 51C452852265093600C03939 /* FlattenedAccountFolderPickerData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452812265093600C03939 /* FlattenedAccountFolderPickerData.swift */; }; 51C452862265093600C03939 /* Add.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51C452822265093600C03939 /* Add.storyboard */; }; 51C452882265093600C03939 /* AddFeedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C452842265093600C03939 /* AddFeedViewController.swift */; }; @@ -816,7 +816,7 @@ 515D4FCE2325B3D000EE1167 /* NetNewsWire_iOSshareextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSshareextension_target.xcconfig; sourceTree = ""; }; 51707438232AA97100A461A3 /* ShareFolderPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareFolderPickerController.swift; sourceTree = ""; }; 517630032336215100E15FFF /* main.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = main.js; sourceTree = ""; }; - 517630222336657E00E15FFF /* DetailViewControllerWebViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailViewControllerWebViewProvider.swift; sourceTree = ""; }; + 517630222336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleViewControllerWebViewProvider.swift; sourceTree = ""; }; 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicLabel.swift; sourceTree = ""; }; 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicImageView.swift; sourceTree = ""; }; 5183CCDC226F1F5C0010922C /* NavigationProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationProgressView.swift; sourceTree = ""; }; @@ -850,7 +850,7 @@ 51C452722265091600C03939 /* MasterTimelineTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterTimelineTableViewCell.swift; sourceTree = ""; }; 51C452742265091600C03939 /* MasterUnreadIndicatorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterUnreadIndicatorView.swift; sourceTree = ""; }; 51C452752265091600C03939 /* MasterTimelineDefaultCellLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterTimelineDefaultCellLayout.swift; sourceTree = ""; }; - 51C4527E2265092C00C03939 /* DetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; + 51C4527E2265092C00C03939 /* ArticleViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleViewController.swift; sourceTree = ""; }; 51C452812265093600C03939 /* FlattenedAccountFolderPickerData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FlattenedAccountFolderPickerData.swift; sourceTree = ""; }; 51C452822265093600C03939 /* Add.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Add.storyboard; sourceTree = ""; }; 51C452842265093600C03939 /* AddFeedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFeedViewController.swift; sourceTree = ""; }; @@ -1372,13 +1372,13 @@ path = Cell; sourceTree = ""; }; - 51C4527D2265092C00C03939 /* Detail */ = { + 51C4527D2265092C00C03939 /* Article */ = { isa = PBXGroup; children = ( - 51C4527E2265092C00C03939 /* DetailViewController.swift */, - 517630222336657E00E15FFF /* DetailViewControllerWebViewProvider.swift */, + 51C4527E2265092C00C03939 /* ArticleViewController.swift */, + 517630222336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift */, ); - path = Detail; + path = Article; sourceTree = ""; }; 51C452802265093600C03939 /* Add */ = { @@ -1959,7 +1959,7 @@ 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */, 51C4525D226508F600C03939 /* MasterFeed */, 51C4526D2265091600C03939 /* MasterTimeline */, - 51C4527D2265092C00C03939 /* Detail */, + 51C4527D2265092C00C03939 /* Article */, 51C452802265093600C03939 /* Add */, 5183CCEB227117C70010922C /* Settings */, 5183CCDB226F1EEB0010922C /* Progress */, @@ -2708,7 +2708,7 @@ 51C452A722650A3D00C03939 /* RSImage-Extensions.swift in Sources */, 51C45269226508F600C03939 /* MasterFeedTableViewCell.swift in Sources */, 51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */, - 517630232336657E00E15FFF /* DetailViewControllerWebViewProvider.swift in Sources */, + 517630232336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift in Sources */, 51C4528F226509BD00C03939 /* UnreadFeed.swift in Sources */, 51AF460E232488C6001742EF /* Account-Extensions.swift in Sources */, 5183CCDD226F1F5C0010922C /* NavigationProgressView.swift in Sources */, @@ -2757,7 +2757,7 @@ 51FA73A82332BE880090D516 /* ExtractedArticle.swift in Sources */, 51C4527C2265091600C03939 /* MasterTimelineDefaultCellLayout.swift in Sources */, 51C4529A22650A0400C03939 /* ArticleStyle.swift in Sources */, - 51C4527F2265092C00C03939 /* DetailViewController.swift in Sources */, + 51C4527F2265092C00C03939 /* ArticleViewController.swift in Sources */, 51C4526A226508F600C03939 /* MasterFeedTableViewCellLayout.swift in Sources */, 51C452AE2265104D00C03939 /* TimelineStringFormatter.swift in Sources */, 512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */, diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 2bce61ac6..9a4757548 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -54,7 +54,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele appDelegate = self // Force lazy initialization of the web view provider so that it can warm up the queue of prepared web views - let _ = DetailViewControllerWebViewProvider.shared + let _ = ArticleViewControllerWebViewProvider.shared AccountManager.shared = AccountManager() AppDefaults.shared = UserDefaults.init(suiteName: "group.\(Bundle.main.bundleIdentifier!)")! diff --git a/iOS/Detail/DetailViewController.swift b/iOS/Article/ArticleViewController.swift similarity index 95% rename from iOS/Detail/DetailViewController.swift rename to iOS/Article/ArticleViewController.swift index bf2e35ad7..99265597b 100644 --- a/iOS/Detail/DetailViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -1,5 +1,5 @@ // -// DetailViewController.swift +// ArticleViewController.swift // NetNewsWire // // Created by Maurice Parker on 4/8/19. @@ -12,7 +12,7 @@ import Account import Articles import SafariServices -enum DetailViewState: Equatable { +enum ArticleViewState: Equatable { case noSelection case multipleSelection case loading @@ -20,7 +20,7 @@ enum DetailViewState: Equatable { case extracted(Article, ExtractedArticle) } -class DetailViewController: UIViewController { +class ArticleViewController: UIViewController { @IBOutlet private weak var nextUnreadBarButtonItem: UIBarButtonItem! @IBOutlet private weak var prevArticleBarButtonItem: UIBarButtonItem! @@ -34,7 +34,7 @@ class DetailViewController: UIViewController { weak var coordinator: SceneCoordinator! - var state: DetailViewState = .noSelection { + var state: ArticleViewState = .noSelection { didSet { if state != oldValue { updateUI() @@ -61,7 +61,7 @@ class DetailViewController: UIViewController { deinit { webView.removeFromSuperview() - DetailViewControllerWebViewProvider.shared.enqueueWebView(webView) + ArticleViewControllerWebViewProvider.shared.enqueueWebView(webView) webView = nil } @@ -73,7 +73,7 @@ class DetailViewController: UIViewController { NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange(_:)), name: UIContentSizeCategory.didChangeNotification, object: nil) - DetailViewControllerWebViewProvider.shared.dequeueWebView() { webView in + ArticleViewControllerWebViewProvider.shared.dequeueWebView() { webView in self.webView = webView self.webViewContainer.addChildAndPin(webView) @@ -248,7 +248,7 @@ class DetailViewController: UIViewController { // MARK: WKNavigationDelegate -extension DetailViewController: WKNavigationDelegate { +extension ArticleViewController: WKNavigationDelegate { func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if navigationAction.navigationType == .linkActivated { @@ -284,7 +284,7 @@ extension DetailViewController: WKNavigationDelegate { // MARK: Private -private extension DetailViewController { +private extension ArticleViewController { func updateProgressIndicatorIfNeeded() { if !(UIDevice.current.userInterfaceIdiom == .pad) { diff --git a/iOS/Detail/DetailViewControllerWebViewProvider.swift b/iOS/Article/ArticleViewControllerWebViewProvider.swift similarity index 92% rename from iOS/Detail/DetailViewControllerWebViewProvider.swift rename to iOS/Article/ArticleViewControllerWebViewProvider.swift index 58c1d7fdf..f221110a1 100644 --- a/iOS/Detail/DetailViewControllerWebViewProvider.swift +++ b/iOS/Article/ArticleViewControllerWebViewProvider.swift @@ -1,5 +1,5 @@ // -// DetailViewControllerWebViewProvider.swift +// ArticleViewControllerWebViewProvider.swift // NetNewsWire-iOS // // Created by Maurice Parker on 9/21/19. @@ -11,9 +11,9 @@ import WebKit /// WKWebView has an awful behavior of a flash to white on first load when in dark mode. /// Keep a queue of WebViews where we've already done a trivial load so that by the time we need them in the UI, they're past the flash-to-shite part of their lifecycle. -class DetailViewControllerWebViewProvider: NSObject, WKNavigationDelegate { +class ArticleViewControllerWebViewProvider: NSObject, WKNavigationDelegate { - static let shared = DetailViewControllerWebViewProvider() + static let shared = ArticleViewControllerWebViewProvider() private let minimumQueueDepth = 3 private let maximumQueueDepth = 6 diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index ef1fd9be4..b15425794 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -7,10 +7,10 @@ - + - + diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 65e2d8abd..0df1641d8 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -36,17 +36,17 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { return rootSplitViewController.children.last as? UISplitViewController } - private var detailViewController: DetailViewController? { - if let detail = masterNavigationController.viewControllers.last as? DetailViewController { + private var articleViewController: ArticleViewController? { + if let detail = masterNavigationController.viewControllers.last as? ArticleViewController { return detail } if let subSplit = subSplitViewController { if let navController = subSplit.viewControllers.last as? UINavigationController { - return navController.topViewController as? DetailViewController + return navController.topViewController as? ArticleViewController } } else { if let navController = rootSplitViewController.viewControllers.last as? UINavigationController { - return navController.topViewController as? DetailViewController + return navController.topViewController as? ArticleViewController } } return nil @@ -289,9 +289,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { masterFeedViewController.coordinator = self masterNavigationController.pushViewController(masterFeedViewController, animated: false) - let detailViewController = UIStoryboard.main.instantiateController(ofType: DetailViewController.self) - detailViewController.coordinator = self - let detailNavigationController = addNavControllerIfNecessary(detailViewController, showButton: false) + let articleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self) + articleViewController.coordinator = self + let detailNavigationController = addNavControllerIfNecessary(articleViewController, showButton: false) rootSplitViewController.showDetailViewController(detailNavigationController, sender: self) configureThreePanelMode(for: size) @@ -558,27 +558,27 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { if article == nil { if rootSplitViewController.isCollapsed { - if masterNavigationController.children.last is DetailViewController { + if masterNavigationController.children.last is ArticleViewController { masterNavigationController.popViewController(animated: !automated) } } else { - detailViewController?.state = .noSelection + articleViewController?.state = .noSelection } masterTimelineViewController?.updateArticleSelection(animate: !automated) return } - if detailViewController == nil { - let detailViewController = UIStoryboard.main.instantiateController(ofType: DetailViewController.self) - detailViewController.coordinator = self - installDetailController(detailViewController, automated: automated) + if articleViewController == nil { + let articleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self) + articleViewController.coordinator = self + installArticleController(articleViewController, automated: automated) } if automated { masterTimelineViewController?.updateArticleSelection(animate: false) } - detailViewController?.state = .article(article!) + articleViewController?.state = .article(article!) if let article = currentArticle { markArticles(Set([article]), statusKey: .read, flag: true) @@ -686,8 +686,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func scrollOrGoToNextUnread() { - if detailViewController?.canScrollDown() ?? false { - detailViewController?.scrollPageDown() + if articleViewController?.canScrollDown() ?? false { + articleViewController?.scrollPageDown() } else { selectNextUnread() } @@ -844,7 +844,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func navigateToDetail() { - detailViewController?.focus() + articleViewController?.focus() } } @@ -1340,16 +1340,16 @@ private extension SceneCoordinator { } } - func installDetailController(_ detailController: UIViewController, automated: Bool) { + func installArticleController(_ articleController: UIViewController, automated: Bool) { if let subSplit = subSplitViewController { - let controller = addNavControllerIfNecessary(detailController, showButton: false) + let controller = addNavControllerIfNecessary(articleController, showButton: false) subSplit.showDetailViewController(controller, sender: self) } else if rootSplitViewController.isCollapsed { - let controller = addNavControllerIfNecessary(detailController, showButton: false) + let controller = addNavControllerIfNecessary(articleController, showButton: false) masterNavigationController.pushViewController(controller, animated: !automated) } else { - let controller = addNavControllerIfNecessary(detailController, showButton: true) + let controller = addNavControllerIfNecessary(articleController, showButton: true) rootSplitViewController.showDetailViewController(controller, sender: self) } @@ -1406,12 +1406,12 @@ private extension SceneCoordinator { } let controller: UIViewController = { - if let result = detailViewController { + if let result = articleViewController { return result } else { - let detailController = UIStoryboard.main.instantiateController(ofType: DetailViewController.self) - detailController.coordinator = self - return detailController + let articleViewController = UIStoryboard.main.instantiateController(ofType: ArticleViewController.self) + articleViewController.coordinator = self + return articleViewController } }() From 026c7cfd6d3fd894c6bf098895fe72864bb7df4a Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 24 Sep 2019 06:46:53 -0500 Subject: [PATCH 3/9] Initial article extractor implementation for iOS --- iOS/Article/ArticleViewController.swift | 5 +++ iOS/Base.lproj/Main.storyboard | 22 +++++++-- iOS/SceneCoordinator.swift | 60 +++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 3 deletions(-) diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index 99265597b..f60a5da49 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -22,6 +22,7 @@ enum ArticleViewState: Equatable { class ArticleViewController: UIViewController { + @IBOutlet private weak var readerViewBarButtonItem: UIBarButtonItem! @IBOutlet private weak var nextUnreadBarButtonItem: UIBarButtonItem! @IBOutlet private weak var prevArticleBarButtonItem: UIBarButtonItem! @IBOutlet private weak var nextArticleBarButtonItem: UIBarButtonItem! @@ -178,6 +179,10 @@ class ArticleViewController: UIViewController { // MARK: Actions + @IBAction func toggleReaderView(_ sender: Any) { + coordinator.toggleArticleExtractor() + } + @IBAction func nextUnread(_ sender: Any) { coordinator.selectNextUnread() } diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index b15425794..ef74ea072 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -16,7 +16,7 @@ - + @@ -98,7 +98,17 @@ - + + + + + + + + + + + @@ -107,6 +117,7 @@ + @@ -152,6 +163,7 @@ + @@ -203,7 +215,10 @@ - + + + + @@ -226,6 +241,7 @@ + diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 0df1641d8..76952f867 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -27,6 +27,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { private var activityManager = ActivityManager() + private var isShowingExtractedArticle = false + private var articleExtractor: ArticleExtractor? = nil + private var rootSplitViewController: RootSplitViewController! private var masterNavigationController: UINavigationController! private var masterFeedViewController: MasterFeedViewController! @@ -795,6 +798,37 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { masterFeedViewController.present(addViewController, animated: true) } + func toggleArticleExtractor() { + + guard let article = currentArticle else { + return + } + + guard articleExtractor?.state != .processing else { + articleExtractor?.cancel() + articleExtractor = nil + isShowingExtractedArticle = false + articleViewController?.state = .article(article) + return + } + + guard !isShowingExtractedArticle else { + isShowingExtractedArticle = false + articleViewController?.state = .article(article) + return + } + + if let articleExtractor = articleExtractor, let extractedArticle = articleExtractor.article { + if currentArticle?.preferredLink == articleExtractor.articleLink { + isShowingExtractedArticle = true + articleViewController?.state = .extracted(article, extractedArticle) + } + } else { + startArticleExtractorForCurrentLink() + } + + } + func homePageURLForFeed(_ indexPath: IndexPath) -> URL? { guard let node = nodeFor(indexPath), let feed = node.representedObject as? Feed, @@ -880,6 +914,24 @@ extension SceneCoordinator: UINavigationControllerDelegate { } +// MARK: ArticleExtractorDelegate + +extension SceneCoordinator: ArticleExtractorDelegate { + + func articleExtractionDidFail(with: Error) { +// makeToolbarValidate() + } + + func articleExtractionDidComplete(extractedArticle: ExtractedArticle) { + if let article = currentArticle, articleExtractor?.state != .cancelled { + isShowingExtractedArticle = true + articleViewController?.state = .extracted(article, extractedArticle) +// makeToolbarValidate() + } + } + +} + // MARK: Private private extension SceneCoordinator { @@ -1181,6 +1233,14 @@ private extension SceneCoordinator { // MARK: Fetching Articles + func startArticleExtractorForCurrentLink() { + if let link = currentArticle?.preferredLink, let extractor = ArticleExtractor(link) { + extractor.delegate = self + extractor.process() + articleExtractor = extractor + } + } + func emptyTheTimeline() { if !articles.isEmpty { replaceArticles(with: Set
(), animate: true) From 46c737c77772ff54f3fed93e8374a556a8f50e5d Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 24 Sep 2019 06:56:39 -0500 Subject: [PATCH 4/9] Make sure delegates get set even if a metadata file isn't found. Issue #1051 --- Frameworks/Account/AccountMetadataFile.swift | 2 +- Frameworks/Account/FeedMetadataFile.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Frameworks/Account/AccountMetadataFile.swift b/Frameworks/Account/AccountMetadataFile.swift index cb1aaed5d..03b5ccf7e 100644 --- a/Frameworks/Account/AccountMetadataFile.swift +++ b/Frameworks/Account/AccountMetadataFile.swift @@ -48,8 +48,8 @@ private extension AccountMetadataFile { if let fileData = try? Data(contentsOf: readURL) { let decoder = PropertyListDecoder() account.metadata = (try? decoder.decode(AccountMetadata.self, from: fileData)) ?? AccountMetadata() - account.metadata.delegate = account } + account.metadata.delegate = account }) if let error = errorPointer?.pointee { diff --git a/Frameworks/Account/FeedMetadataFile.swift b/Frameworks/Account/FeedMetadataFile.swift index a602819b6..42fb8a460 100644 --- a/Frameworks/Account/FeedMetadataFile.swift +++ b/Frameworks/Account/FeedMetadataFile.swift @@ -48,10 +48,10 @@ private extension FeedMetadataFile { if let fileData = try? Data(contentsOf: readURL) { let decoder = PropertyListDecoder() account.feedMetadata = (try? decoder.decode(Account.FeedMetadataDictionary.self, from: fileData)) ?? Account.FeedMetadataDictionary() - account.feedMetadata.values.forEach { $0.delegate = account } - if !account.startingUp { - account.resetFeedMetadataAndUnreadCounts() - } + } + account.feedMetadata.values.forEach { $0.delegate = account } + if !account.startingUp { + account.resetFeedMetadataAndUnreadCounts() } }) From 6a6d2b8a270923ee277b8753f30db2a3cdbe0da3 Mon Sep 17 00:00:00 2001 From: Phil Viso Date: Tue, 24 Sep 2019 07:01:22 -0500 Subject: [PATCH 5/9] Fixed incorrect articles being restored as part of state restoration --- Shared/Activity/ActivityManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Shared/Activity/ActivityManager.swift b/Shared/Activity/ActivityManager.swift index dafc7e854..efb30230c 100644 --- a/Shared/Activity/ActivityManager.swift +++ b/Shared/Activity/ActivityManager.swift @@ -119,8 +119,8 @@ class ActivityManager { } func invalidateReading() { - nextUnreadActivity?.invalidate() - nextUnreadActivity = nil + readingActivity?.invalidate() + readingActivity = nil } static func cleanUp(_ account: Account) { From 59b0206e235919a3a5f40f67a942ffc27cf3e143 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 24 Sep 2019 08:17:02 -0500 Subject: [PATCH 6/9] Change how we are handling secrets --- .../Article Extractor/ArticleExtractor.swift | 6 ++--- .../ArticleExtractorConfig.swift | 26 +++---------------- 2 files changed, 6 insertions(+), 26 deletions(-) diff --git a/Shared/Article Extractor/ArticleExtractor.swift b/Shared/Article Extractor/ArticleExtractor.swift index 7390cf94e..281fe601d 100644 --- a/Shared/Article Extractor/ArticleExtractor.swift +++ b/Shared/Article Extractor/ArticleExtractor.swift @@ -35,9 +35,9 @@ class ArticleExtractor { public init?(_ articleLink: String) { self.articleLink = articleLink - let clientURL = ArticleExtractorConfig.Mercury.clientURL - let username = ArticleExtractorConfig.Mercury.clientId - let signiture = articleLink.hmacUsingSHA1(key: ArticleExtractorConfig.Mercury.clientSecret) + let clientURL = ArticleExtractorConfig.clientURL + let username = ArticleExtractorConfig.clientId + let signiture = articleLink.hmacUsingSHA1(key: ArticleExtractorConfig.clientSecret) if let base64URL = articleLink.data(using: .utf8)?.base64EncodedString() { let fullURL = "\(clientURL)/\(username)/\(signiture)?base64_url=\(base64URL)" diff --git a/Shared/Article Extractor/ArticleExtractorConfig.swift b/Shared/Article Extractor/ArticleExtractorConfig.swift index 0fa5fb576..7f4518fd4 100644 --- a/Shared/Article Extractor/ArticleExtractorConfig.swift +++ b/Shared/Article Extractor/ArticleExtractorConfig.swift @@ -9,27 +9,7 @@ import Foundation enum ArticleExtractorConfig { - - enum Mercury { - // For testing add the environment variables in the scheme you are using - static let clientId = ArticleExtractorConfig.environmentVariable(named: "MERCURY_CLIENT_ID") ?? Release.mercuryId - static let clientSecret = ArticleExtractorConfig.environmentVariable(named: "MERCURY_CLIENT_SECRET") ?? Release.mercurySecret - static let clientURL = Release.mercuryURL - } - - private enum Release { - static let mercuryId = "{MERCURYID}" - static let mercurySecret = "{MERCURYSECRET}" - static let mercuryURL = "https://extract.feedbin.com/parser" - } - - private static func environmentVariable(named: String) -> String? { - let processInfo = ProcessInfo.processInfo - guard let value = processInfo.environment[named] else { - print("‼️ Missing Environment Variable: '\(named)'") - return nil - } - return value - } - + static let clientId = "{MERCURYID}" + static let clientSecret = "{MERCURYSECRET}" + static let clientURL = "https://extract.feedbin.com/parser" } From 2c095e6dfebd4881e9e3df14f52340bb51bd00bb Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 24 Sep 2019 08:41:00 -0500 Subject: [PATCH 7/9] Modify how secrets are inserted into the build process --- NetNewsWire.xcodeproj/project.pbxproj | 65 +++++++++++++++++-- xcconfig/NetNewsWire_iOSapp_target.xcconfig | 1 + xcconfig/NetNewsWire_project.xcconfig | 2 + xcconfig/NetNewsWire_project_release.xcconfig | 1 - 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 8baf5d9c1..3ed1b7a91 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -2156,7 +2156,9 @@ isa = PBXNativeTarget; buildConfigurationList = 840D61A32029031E009BC708 /* Build configuration list for PBXNativeTarget "NetNewsWire-iOS" */; buildPhases = ( + 517D2D80233A46ED00FF3E35 /* Update ArticleExtractorConfig.swift */, 840D61782029031C009BC708 /* Sources */, + 517D2D81233A47AD00FF3E35 /* Reset ArticleExtractorConfig.swift */, 840D61792029031C009BC708 /* Frameworks */, 840D617A2029031C009BC708 /* Resources */, 51C451DF2264C7F200C03939 /* Embed Frameworks */, @@ -2186,8 +2188,9 @@ isa = PBXNativeTarget; buildConfigurationList = 849C647A1ED37A5D003D8FC0 /* Build configuration list for PBXNativeTarget "NetNewsWire" */; buildPhases = ( - 51D6803823330CFF0097A009 /* Update Feedbin Mercury API Keys */, + 51D6803823330CFF0097A009 /* Update ArticleExtractorConfig.swift */, 849C645C1ED37A5D003D8FC0 /* Sources */, + 517D2D82233A53D600FF3E35 /* Reset ArticleExtractorConfig.swift */, 849C645D1ED37A5D003D8FC0 /* Frameworks */, 849C645E1ED37A5D003D8FC0 /* Resources */, 84C987A52000AC9E0066B150 /* Run Script: Automated build numbers */, @@ -2597,7 +2600,7 @@ shellPath = /bin/sh; shellScript = "xcrun -sdk macosx swiftc -target x86_64-macosx10.11 buildscripts/VerifyNoBuildSettings.swift -o $CONFIGURATION_TEMP_DIR/VerifyNoBS\n$CONFIGURATION_TEMP_DIR/VerifyNoBS ${PROJECT_NAME}.xcodeproj/project.pbxproj\n"; }; - 51D6803823330CFF0097A009 /* Update Feedbin Mercury API Keys */ = { + 517D2D80233A46ED00FF3E35 /* Update ArticleExtractorConfig.swift */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -2606,14 +2609,68 @@ ); inputPaths = ( ); - name = "Update Feedbin Mercury API Keys"; + name = "Update ArticleExtractorConfig.swift"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "FAILED=false\n\nif [ \"${CONFIGURATION}\" = \"Debug\" ]; then\necho \"Not a release build. ArticleExtractorConfig.swift not changed.\"\nexit 0\nfi\n\nif [ -z \"${MERCURY_CLIENT_ID}\" ]; then\necho \"${SRCROOT}/Shared/Article Extractor/ArticleExtractorConfig.swift:21:1: warning: Missing Feedbin Mercury Client ID\"\nFAILED=true\nfi\n\nif [ -z \"${MERCURY_CLIENT_SECRET}\" ]; then\necho \"${SRCROOT}/Shared/Article Extractor/ArticleExtractorConfig.swift:22:1: warning: Missing Feedbin Mercury Client Secret\"\nFAILED=true\nfi\n\nsed -i .tmp \"s|{MERCURYID}|${MERCURY_CLIENT_ID}|g; s|{MERCURYSECRET}|${MERCURY_CLIENT_SECRET}|g\" \"${SRCROOT}/Shared/Article Extractor/ArticleExtractorConfig.swift\"\n\nrm -f \"${SRCROOT}/Shared/Article Extractor/ArticleExtractorConfig.swift.tmp\"\n\nif [ \"$FAILED\" = true ]; then\nexit 1\nfi\n\necho \"All env values found!\"\n"; + shellScript = "FAILED=false\n\nif [ -z \"${MERCURY_CLIENT_ID}\" ]; then\nFAILED=true\nfi\n\nif [ -z \"${MERCURY_CLIENT_SECRET}\" ]; then\nFAILED=true\nfi\n\nif [ \"$FAILED\" = true ]; then\necho \"Missing Feedbin Mercury credetials. ArticleExtractorConfig.swift not changed.\"\nexit 0\nfi\n\nsed -i .tmp \"s|{MERCURYID}|${MERCURY_CLIENT_ID}|g; s|{MERCURYSECRET}|${MERCURY_CLIENT_SECRET}|g\" \"${SRCROOT}/Shared/Article Extractor/ArticleExtractorConfig.swift\"\n\nrm -f \"${SRCROOT}/Shared/Article Extractor/ArticleExtractorConfig.swift.tmp\"\n\necho \"All env values found!\"\n"; + }; + 517D2D81233A47AD00FF3E35 /* Reset ArticleExtractorConfig.swift */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Reset ArticleExtractorConfig.swift"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "git checkout \"${SRCROOT}/Shared/Article Extractor/ArticleExtractorConfig.swift\"\n"; + }; + 517D2D82233A53D600FF3E35 /* Reset ArticleExtractorConfig.swift */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Reset ArticleExtractorConfig.swift"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "git checkout \"${SRCROOT}/Shared/Article Extractor/ArticleExtractorConfig.swift\"\n"; + }; + 51D6803823330CFF0097A009 /* Update ArticleExtractorConfig.swift */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Update ArticleExtractorConfig.swift"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "FAILED=false\n\nif [ -z \"${MERCURY_CLIENT_ID}\" ]; then\nFAILED=true\nfi\n\nif [ -z \"${MERCURY_CLIENT_SECRET}\" ]; then\nFAILED=true\nfi\n\nif [ \"$FAILED\" = true ]; then\necho \"Missing Feedbin Mercury credetials. ArticleExtractorConfig.swift not changed.\"\nexit 0\nfi\n\nsed -i .tmp \"s|{MERCURYID}|${MERCURY_CLIENT_ID}|g; s|{MERCURYSECRET}|${MERCURY_CLIENT_SECRET}|g\" \"${SRCROOT}/Shared/Article Extractor/ArticleExtractorConfig.swift\"\n\nrm -f \"${SRCROOT}/Shared/Article Extractor/ArticleExtractorConfig.swift.tmp\"\n\necho \"All env values found!\"\n\n"; }; 8423E3E3220158E700C3795B /* Run Script: codesign release builds */ = { isa = PBXShellScriptBuildPhase; diff --git a/xcconfig/NetNewsWire_iOSapp_target.xcconfig b/xcconfig/NetNewsWire_iOSapp_target.xcconfig index a10ee2172..95779eaa8 100644 --- a/xcconfig/NetNewsWire_iOSapp_target.xcconfig +++ b/xcconfig/NetNewsWire_iOSapp_target.xcconfig @@ -30,6 +30,7 @@ PROVISIONING_PROFILE_SPECIFIER = // /Users/Shared/git/SharedXcodeSettings/DeveloperSettings.xcconfig // +#include? "../../SharedXcodeSettings/ProjectSettings.xcconfig" #include? "../../SharedXcodeSettings/DeveloperSettings.xcconfig" ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES diff --git a/xcconfig/NetNewsWire_project.xcconfig b/xcconfig/NetNewsWire_project.xcconfig index fbb95fcf1..d69ff02a4 100644 --- a/xcconfig/NetNewsWire_project.xcconfig +++ b/xcconfig/NetNewsWire_project.xcconfig @@ -1,3 +1,5 @@ +#include? "../../SharedXcodeSettings/ProjectSettings.xcconfig" + ALWAYS_SEARCH_USER_PATHS = NO CLANG_ANALYZER_NONNULL = YES CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; diff --git a/xcconfig/NetNewsWire_project_release.xcconfig b/xcconfig/NetNewsWire_project_release.xcconfig index 4756e91a1..f4d9e6289 100644 --- a/xcconfig/NetNewsWire_project_release.xcconfig +++ b/xcconfig/NetNewsWire_project_release.xcconfig @@ -1,4 +1,3 @@ -#include? "../../SharedXcodeSettings/ReleaseSettings.xcconfig" #include "./NetNewsWire_project.xcconfig" COPY_PHASE_STRIP = NO From eb6996789994975803dbf013d102f8f54edab4fa Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 24 Sep 2019 11:32:54 -0500 Subject: [PATCH 8/9] Clear article extractor when article selection changes --- iOS/SceneCoordinator.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 76952f867..d75fd4536 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -556,6 +556,11 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func selectArticle(_ article: Article?, automated: Bool = true) { guard article != currentArticle else { return } + articleExtractor?.cancel() + articleExtractor = nil + isShowingExtractedArticle = false +// makeToolbarValidate() + currentArticle = article activityManager.reading(currentArticle) From 98befac78c074022b98e3c114f03451f59666f83 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Tue, 24 Sep 2019 16:34:11 -0500 Subject: [PATCH 9/9] Animate reader view button --- Mac/MainWindow/ArticleExtractorButton.swift | 4 +- NetNewsWire.xcodeproj/project.pbxproj | 4 + iOS/AppAssets.swift | 22 +++++ iOS/Article/ArticleExtractorButton.swift | 78 ++++++++++++++++++ iOS/Article/ArticleViewController.swift | 18 +++- iOS/Base.lproj/Main.storyboard | 17 ++-- .../ArticleExtractorOff.pdf | Bin 0 -> 4289 bytes .../Contents.json | 15 ++++ .../ArticleExtractorOff.pdf | Bin 0 -> 4289 bytes .../Contents.json | 15 ++++ .../ArticleExtractorOn.pdf | Bin 0 -> 4197 bytes .../articleExtractorOn.imageset/Contents.json | 15 ++++ iOS/SceneCoordinator.swift | 10 ++- 13 files changed, 182 insertions(+), 16 deletions(-) create mode 100644 iOS/Article/ArticleExtractorButton.swift create mode 100644 iOS/Resources/Assets.xcassets/articleExtractorError.imageset/ArticleExtractorOff.pdf create mode 100644 iOS/Resources/Assets.xcassets/articleExtractorError.imageset/Contents.json create mode 100644 iOS/Resources/Assets.xcassets/articleExtractorOff.imageset/ArticleExtractorOff.pdf create mode 100644 iOS/Resources/Assets.xcassets/articleExtractorOff.imageset/Contents.json create mode 100644 iOS/Resources/Assets.xcassets/articleExtractorOn.imageset/ArticleExtractorOn.pdf create mode 100644 iOS/Resources/Assets.xcassets/articleExtractorOn.imageset/Contents.json diff --git a/Mac/MainWindow/ArticleExtractorButton.swift b/Mac/MainWindow/ArticleExtractorButton.swift index 4fd04bf45..69e250ed5 100644 --- a/Mac/MainWindow/ArticleExtractorButton.swift +++ b/Mac/MainWindow/ArticleExtractorButton.swift @@ -57,7 +57,7 @@ class ArticleExtractorButton: NSButton { case isError: addImageSublayer(to: hostedLayer, image: AppAssets.articleExtractorError, opacity: opacity) case isInProgress: - addProgressSublayer(to: hostedLayer) + addAnimatedSublayer(to: hostedLayer) default: addImageSublayer(to: hostedLayer, image: AppAssets.articleExtractor, opacity: opacity) } @@ -77,7 +77,7 @@ class ArticleExtractorButton: NSButton { hostedLayer.addSublayer(imageLayer) } - private func addProgressSublayer(to hostedLayer: CALayer) { + private func addAnimatedSublayer(to hostedLayer: CALayer) { let imageProgress1 = AppAssets.articleExtractorProgress1 let imageProgress2 = AppAssets.articleExtractorProgress2 let imageProgress3 = AppAssets.articleExtractorProgress3 diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 3ed1b7a91..4f88ef057 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 510BD15D232D765D002692E4 /* SettingsReaderAPIAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557EE1A522B6F4E1004206FA /* SettingsReaderAPIAccountView.swift */; }; + 51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */; }; 51126DA4225FDE2F00722696 /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; }; 5115CAF42266301400B21BCE /* AddContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */; }; 511D43CF231FA62200FB1562 /* DetailKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */; }; @@ -777,6 +778,7 @@ 510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsLocalAccountView.swift; sourceTree = ""; }; 510D707F22B02A5F004E8F65 /* SettingsFeedbinAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFeedbinAccountView.swift; sourceTree = ""; }; 510D708122B041CC004E8F65 /* SettingsAccountLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountLabelView.swift; sourceTree = ""; }; + 51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorButton.swift; sourceTree = ""; }; 51121AA12265430A00BC0EC1 /* NetNewsWire_iOSapp_target.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSapp_target.xcconfig; sourceTree = ""; }; 51121B5A22661FEF00BC0EC1 /* AddContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContainerViewController.swift; sourceTree = ""; }; 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-Extensions.swift"; sourceTree = ""; }; @@ -1377,6 +1379,7 @@ children = ( 51C4527E2265092C00C03939 /* ArticleViewController.swift */, 517630222336657E00E15FFF /* ArticleViewControllerWebViewProvider.swift */, + 51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */, ); path = Article; sourceTree = ""; @@ -2823,6 +2826,7 @@ 51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */, 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */, 51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */, + 51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */, 84CAFCA522BC8C08007694F0 /* FetchRequestQueue.swift in Sources */, 51C4529C22650A1000C03939 /* SingleFaviconDownloader.swift in Sources */, 51E595A6228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */, diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index 4cf148530..d669980d0 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -10,6 +10,28 @@ import RSCore struct AppAssets { + static var articleExtractorError: UIImage = { + return UIImage(named: "articleExtractorOff")! + }() + + static var articleExtractorOff: UIImage = { + return UIImage(named: "articleExtractorOff")! + }() + + static var articleExtractorOffTinted: UIImage = { + let image = UIImage(named: "articleExtractorOff")! + return image.maskWithColor(color: AppAssets.primaryAccentColor.cgColor)! + }() + + static var articleExtractorOn: UIImage = { + return UIImage(named: "articleExtractorOn")! + }() + + static var articleExtractorOnTinted: UIImage = { + let image = UIImage(named: "articleExtractorOn")! + return image.maskWithColor(color: AppAssets.primaryAccentColor.cgColor)! + }() + static var avatarBackgroundColor: UIColor = { return UIColor(named: "avatarBackgroundColor")! }() diff --git a/iOS/Article/ArticleExtractorButton.swift b/iOS/Article/ArticleExtractorButton.swift new file mode 100644 index 000000000..8c4641964 --- /dev/null +++ b/iOS/Article/ArticleExtractorButton.swift @@ -0,0 +1,78 @@ +// +// ArticleExtractorButton.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 9/24/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit + +enum ArticleExtractorButtonState { + case error + case animated + case on + case off +} + +class ArticleExtractorButton: UIButton { + + var buttonState: ArticleExtractorButtonState = .off { + didSet { + if buttonState != oldValue { + switch buttonState { + case .error: + stripSublayer() + setImage(AppAssets.articleExtractorError, for: .normal) + case .animated: + setImage(nil, for: .normal) + setNeedsLayout() + case .on: + stripSublayer() + setImage(AppAssets.articleExtractorOn, for: .normal) + case .off: + stripSublayer() + setImage(AppAssets.articleExtractorOff, for: .normal) + } + } + } + } + + override func layoutSubviews() { + super.layoutSubviews() + guard case .animated = buttonState else { + return + } + stripSublayer() + addAnimatedSublayer(to: layer) + } + + private func stripSublayer() { + if layer.sublayers?.count ?? 0 > 1 { + layer.sublayers?.last?.removeFromSuperlayer() + } + } + + private func addAnimatedSublayer(to hostedLayer: CALayer) { + let image1 = AppAssets.articleExtractorOffTinted.cgImage! + let image2 = AppAssets.articleExtractorOnTinted.cgImage! + let images = [image1, image2, image1] + + let imageLayer = CALayer() + let imageSize = AppAssets.articleExtractorOff.size + imageLayer.bounds = CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height) + imageLayer.position = CGPoint(x: bounds.midX, y: bounds.midY) + + hostedLayer.addSublayer(imageLayer) + + let animation = CAKeyframeAnimation(keyPath: "contents") + animation.calculationMode = CAAnimationCalculationMode.linear + animation.keyTimes = [0, 0.5, 1] + animation.duration = 2 + animation.values = images as [Any] + animation.repeatCount = HUGE + + imageLayer.add(animation, forKey: "contents") + } + +} diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index f60a5da49..5684e0572 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -22,7 +22,7 @@ enum ArticleViewState: Equatable { class ArticleViewController: UIViewController { - @IBOutlet private weak var readerViewBarButtonItem: UIBarButtonItem! + @IBOutlet private weak var articleExtractorButton: ArticleExtractorButton! @IBOutlet private weak var nextUnreadBarButtonItem: UIBarButtonItem! @IBOutlet private weak var prevArticleBarButtonItem: UIBarButtonItem! @IBOutlet private weak var nextArticleBarButtonItem: UIBarButtonItem! @@ -55,6 +55,15 @@ class ArticleViewController: UIViewController { } } + var articleExtractorButtonState: ArticleExtractorButtonState { + get { + return articleExtractorButton.buttonState + } + set { + articleExtractorButton.buttonState = newValue + } + } + private let keyboardManager = KeyboardManager(type: .detail) override var keyCommands: [UIKeyCommand]? { return keyboardManager.keyCommands @@ -74,6 +83,9 @@ class ArticleViewController: UIViewController { NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange(_:)), name: UIContentSizeCategory.didChangeNotification, object: nil) + // For some reason interface builder won't let me set this there. + articleExtractorButton.addTarget(self, action: #selector(toggleArticleExtractor(_:)), for: .touchUpInside) + ArticleViewControllerWebViewProvider.shared.dequeueWebView() { webView in self.webView = webView @@ -96,6 +108,7 @@ class ArticleViewController: UIViewController { func updateUI() { guard let article = currentArticle else { + articleExtractorButton.isEnabled = false nextUnreadBarButtonItem.isEnabled = false prevArticleBarButtonItem.isEnabled = false nextArticleBarButtonItem.isEnabled = false @@ -110,6 +123,7 @@ class ArticleViewController: UIViewController { prevArticleBarButtonItem.isEnabled = coordinator.isPrevArticleAvailable nextArticleBarButtonItem.isEnabled = coordinator.isNextArticleAvailable + articleExtractorButton.isEnabled = true readBarButtonItem.isEnabled = true starBarButtonItem.isEnabled = true browserBarButtonItem.isEnabled = true @@ -179,7 +193,7 @@ class ArticleViewController: UIViewController { // MARK: Actions - @IBAction func toggleReaderView(_ sender: Any) { + @IBAction func toggleArticleExtractor(_ sender: Any) { coordinator.toggleArticleExtractor() } diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index ef74ea072..17f5e4a52 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -99,25 +99,24 @@ - - - - - - - + + + - @@ -240,8 +239,8 @@ + - diff --git a/iOS/Resources/Assets.xcassets/articleExtractorError.imageset/ArticleExtractorOff.pdf b/iOS/Resources/Assets.xcassets/articleExtractorError.imageset/ArticleExtractorOff.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2df7fdd06dcf42ba2126e3e59c29918e100ead66 GIT binary patch literal 4289 zcmai&2UHVVx5p__ARwYNDe8a}Q6vdTC`#`rN)seh(}2)|BoskJkaB6#r6?j@K#B$x z!GLrTFA)I&5iruEOA(NWd_leMdf$C-eY0lGoVCxM|2nhx|E%?!kv7vaH~~|DgQZ(0 zmM3P5SMI)UX#*nwC_u!xfz{LihzZ`yh3pC-nIQ`RVnFa9<9(QS4>TFChsP3ectAq~ zOd|W>(VpM{R(z`V4RHw`{dHfk0v`rj_2)_13@ctU3q;bfBm+{2T+V_bl<+0buJX?* z2K#p-;`9+?AxJAf-p`kK=etw|NzAkg*1C_Y(KzuK&r zv(5(X?rtuDQ4OUAl2sE_SAHDop$*)Qb34h^@|$H+q;-Km|3>1>qZ_Zo_Ox4>X=KHx zrP@L6abGHU;ZWFg0oh|=u(DUN;F!S4AxxqhN{nA1kGI{f265ipT0C1k#f1BHOesOo zwlOcakheWS{02SOcSW#S<t@H0QJ0#9BJ__`IKAO|Tuh>LPh|5hRLtaeaG7`$>ccJDXT8-rMc^j4INwm4 zMO-$ef>avUyu;`WUfJI3JpOcvfq_rV9`bKdi>h2mR@HmlymANF#Z|qGDO&zmH&~V~aUk$n zb{E+>3Z89ja8k3!iAVWtL(lr*oZDSAy#(6S?PNzf8p(?Ras=fIsdkN;{J~L&e{0dj7IE9)Osk zU4C4B@Lps9{?oD+coNaq2a6{GNx8q>3`mGP$-HmTF096;a#YlN+#DMT2Ws>WFdN)jfFF{MfI5vZ|ql9*E=i$MK0qpplD-E(Z2o+!o;T+ z&YMdV%dZ&N@zmzC^gjYjXsgvcH=x4TDv3AsS*1~)nXXCm<>K@6sJD}@UmFHT4_&p~ zQhyQ1uv(ItqutStyMr3_n7a?53bCw7+1@%xsZXUw790>k=Lk+5dy5o19(UVoyfxaf@D zt5-a{=``HdI^Xu#b5+8h9nJE$bDj$?MSQX{|CGuV9_?Bep%!YBsDJ~9hxE}y4k*cT zh+NHf7jX4;VhN5=HLEe^ctKQW(LKU0<)sL-_d+{dA3x2Je$5VY=97 z7WoLLrW8)C7o6&`dhV1sZ~JqRkK*1}YYx~edLb)7Pxki5)m@uy-b0sJ3V3#GL|`#&ZzJh5=88l; zI7B?;&}hQpwg%ZmE?ba$*j|a7fRu;Sc|K9jDbB6%eN71S(0it2$w2R8<^ zpBanGoJ$cfj+7F%kdn)+4n1$|Cg~ufE<=?_={Siq^f`diax|@w$^o3D_a%Vtf2s)J#=vbYRV<+n44U z^_djWEYW&V5m77AbVM6cB+DzSBWogS3ejHeYRmlyWIJIy^~e#Lmbviiyt6bVRyDW0 zV!rr9w?@HmnR+?)9>vBb{~V=@-yvDSPFeej{S!-n9n&)q8;Db-`C_hyc}{swL4#p~ zUC7JluYq?LW3_KJ-ph^2Z9v~a=hLxAN|VaEkLBCupK4RVKE4#Qq%meEQx%XO22qBrYtO5Ck3!uyczaj1S?$G@{L8n#9_Ln%wTaCZ8WwVr zpO?2hF)M!enzId@9SKYFMc5 zTpewVcTJKZ!+>$9M+HPtdDjQXen;!VgZuW)exlcOEyay-$Og$i=rJxH%|>;}b?xcO z=jIXqEZlqIfKT@&dX2bGk^)X4&MCkiAh z|9NV&pJ5780o~{N1TFv>fUG$*xD~nf@A3sTH?lU?g&l^xagws{6=Ku*rt24>#)af@ z5ZojkP>@I5mzld`=g*m$8cLjDFFhOPR2hG+GI^wagv zq}#}&DB}ikwsAm_y3k=U!5}F=pOW=A!)Kz#=01YQ78Kz2Hz{-Pd>&1dbkZskof=V> zjvW#^d~lR+JDR_#WWuCk)#<7K1gJlyE{c&n)pmR;e#v12|1AlW9{w=ATQ9xEAsTC3 zVJv7o#%8Mh=!5S=r{&P`lGMc1d^vbYcgdfG@am1~9vx|POx1A*xVkJA(u~N5>ejv_ z^etHN=V^5(nH(jdoa-`fd^h=G6eo2{`Z|i6;`VUF#SowK()`uF$<^_!aj(vU9V;D- zt4L?0zpqQZ-}lIGjmQOe-tvgI74}0<&-ptv*3!S!&n9>Ro<@V8I}JMH@nWN1F(bX^ zcg?HnFS=3|C);$M>X_51C%Wu-Jz6~kJ?4j5`7!Uof2?V?LTnzfYbWMFn5@ zI{a!ae4bjkR><)G;K%9zet5ce#U>MjavXeJ@xh^d;?;8A@{_s?bM@9}yGs}M5)yC; z)r$8XQcqX98TJ*=6wHK*CS*-gXM�XTNJ7>TekJ4C!HwXK&@uI3ytcEab(ynR~gm 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�XTNJ7>TekJ4C!HwXK&@uI3ytcEab(ynR~gm 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 j99C9F79$V-?~