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 000000000..2df7fdd06 Binary files /dev/null and b/iOS/Resources/Assets.xcassets/articleExtractorError.imageset/ArticleExtractorOff.pdf differ diff --git a/iOS/Resources/Assets.xcassets/articleExtractorError.imageset/Contents.json b/iOS/Resources/Assets.xcassets/articleExtractorError.imageset/Contents.json new file mode 100644 index 000000000..da8081b60 --- /dev/null +++ b/iOS/Resources/Assets.xcassets/articleExtractorError.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ArticleExtractorOff.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/iOS/Resources/Assets.xcassets/articleExtractorOff.imageset/ArticleExtractorOff.pdf b/iOS/Resources/Assets.xcassets/articleExtractorOff.imageset/ArticleExtractorOff.pdf new file mode 100644 index 000000000..2df7fdd06 Binary files /dev/null and b/iOS/Resources/Assets.xcassets/articleExtractorOff.imageset/ArticleExtractorOff.pdf differ diff --git a/iOS/Resources/Assets.xcassets/articleExtractorOff.imageset/Contents.json b/iOS/Resources/Assets.xcassets/articleExtractorOff.imageset/Contents.json new file mode 100644 index 000000000..da8081b60 --- /dev/null +++ b/iOS/Resources/Assets.xcassets/articleExtractorOff.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ArticleExtractorOff.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/iOS/Resources/Assets.xcassets/articleExtractorOn.imageset/ArticleExtractorOn.pdf b/iOS/Resources/Assets.xcassets/articleExtractorOn.imageset/ArticleExtractorOn.pdf new file mode 100644 index 000000000..07c0a9a30 Binary files /dev/null and b/iOS/Resources/Assets.xcassets/articleExtractorOn.imageset/ArticleExtractorOn.pdf differ diff --git a/iOS/Resources/Assets.xcassets/articleExtractorOn.imageset/Contents.json b/iOS/Resources/Assets.xcassets/articleExtractorOn.imageset/Contents.json new file mode 100644 index 000000000..2617bbdfe --- /dev/null +++ b/iOS/Resources/Assets.xcassets/articleExtractorOn.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ArticleExtractorOn.pdf" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index d75fd4536..cdfacaa43 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -559,7 +559,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { articleExtractor?.cancel() articleExtractor = nil isShowingExtractedArticle = false -// makeToolbarValidate() + articleViewController?.articleExtractorButtonState = .off currentArticle = article activityManager.reading(currentArticle) @@ -813,12 +813,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { articleExtractor?.cancel() articleExtractor = nil isShowingExtractedArticle = false + articleViewController?.articleExtractorButtonState = .off articleViewController?.state = .article(article) return } guard !isShowingExtractedArticle else { isShowingExtractedArticle = false + articleViewController?.articleExtractorButtonState = .off articleViewController?.state = .article(article) return } @@ -826,6 +828,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { if let articleExtractor = articleExtractor, let extractedArticle = articleExtractor.article { if currentArticle?.preferredLink == articleExtractor.articleLink { isShowingExtractedArticle = true + articleViewController?.articleExtractorButtonState = .on articleViewController?.state = .extracted(article, extractedArticle) } } else { @@ -924,14 +927,14 @@ extension SceneCoordinator: UINavigationControllerDelegate { extension SceneCoordinator: ArticleExtractorDelegate { func articleExtractionDidFail(with: Error) { -// makeToolbarValidate() + articleViewController?.articleExtractorButtonState = .error } func articleExtractionDidComplete(extractedArticle: ExtractedArticle) { if let article = currentArticle, articleExtractor?.state != .cancelled { isShowingExtractedArticle = true articleViewController?.state = .extracted(article, extractedArticle) -// makeToolbarValidate() + articleViewController?.articleExtractorButtonState = .on } } @@ -1243,6 +1246,7 @@ private extension SceneCoordinator { extractor.delegate = self extractor.process() articleExtractor = extractor + articleViewController?.articleExtractorButtonState = .animated } }