From 08618f5f1509bbfd9ec76d2118898d92af721ff3 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Thu, 10 Feb 2022 12:13:06 +0800 Subject: [PATCH 1/3] Articles This commit focuses on the `ArticleViewController`: - Adds an "Aa" menu to the article view that allows for theme changes and full screen article toggling on iPhone, and theme changes on iPad. - Removes separate Settings option for full screen availability on iPhone. - Removes tap target in nav bar to enable full screen mode. --- iOS/AppAssets.swift | 4 + iOS/AppDefaults.swift | 9 -- iOS/Article/ArticleViewController.swift | 156 ++++++++++++++-------- iOS/Article/WebViewController.swift | 4 +- iOS/Base.lproj/Main.storyboard | 7 + iOS/Settings/Settings.storyboard | 128 ++++++------------ iOS/Settings/SettingsViewController.swift | 14 -- 7 files changed, 158 insertions(+), 164 deletions(-) diff --git a/iOS/AppAssets.swift b/iOS/AppAssets.swift index 160e5bf2e..bbc6cd424 100644 --- a/iOS/AppAssets.swift +++ b/iOS/AppAssets.swift @@ -249,6 +249,10 @@ struct AppAssets { let image = UIImage(systemName: "star.fill")! return IconImage(image, isSymbol: true, isBackgroundSupressed: true, preferredColor: AppAssets.starColor.cgColor) } + + static var themeImage: UIImage = { + return UIImage(systemName: "doc.richtext")! + }() static var tickMarkColor: UIColor = { return UIColor(named: "tickMarkColor")! diff --git a/iOS/AppDefaults.swift b/iOS/AppDefaults.swift index 04b0fbadc..1d29bbc41 100644 --- a/iOS/AppDefaults.swift +++ b/iOS/AppDefaults.swift @@ -168,15 +168,6 @@ final class AppDefaults { } } - var articleFullscreenAvailable: Bool { - get { - return AppDefaults.bool(for: Key.articleFullscreenAvailable) - } - set { - AppDefaults.setBool(for: Key.articleFullscreenAvailable, newValue) - } - } - var articleFullscreenEnabled: Bool { get { return AppDefaults.bool(for: Key.articleFullscreenEnabled) diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index 9ae9c52b7..69fc555fb 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -15,16 +15,17 @@ import SafariServices class ArticleViewController: UIViewController, MainControllerIdentifiable { typealias State = (extractedArticle: ExtractedArticle?, - isShowingExtractedArticle: Bool, - articleExtractorButtonState: ArticleExtractorButtonState, - windowScrollY: Int) - + isShowingExtractedArticle: Bool, + articleExtractorButtonState: ArticleExtractorButtonState, + windowScrollY: Int) + @IBOutlet private weak var nextUnreadBarButtonItem: UIBarButtonItem! @IBOutlet private weak var prevArticleBarButtonItem: UIBarButtonItem! @IBOutlet private weak var nextArticleBarButtonItem: UIBarButtonItem! @IBOutlet private weak var readBarButtonItem: UIBarButtonItem! @IBOutlet private weak var starBarButtonItem: UIBarButtonItem! @IBOutlet private weak var actionBarButtonItem: UIBarButtonItem! + @IBOutlet private weak var appearanceBarButtonItem: UIBarButtonItem! @IBOutlet private var searchBar: ArticleSearchBar! @IBOutlet private var searchBarBottomConstraint: NSLayoutConstraint! @@ -48,7 +49,7 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { weak var coordinator: SceneCoordinator! private let poppableDelegate = PoppableGestureRecognizerDelegate() - + var article: Article? { didSet { if let controller = currentWebViewController, controller.article != article { @@ -88,19 +89,12 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { override func viewDidLoad() { super.viewDidLoad() - + NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange(_:)), name: UIContentSizeCategory.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) - - let fullScreenTapZone = UIView() - NSLayoutConstraint.activate([ - fullScreenTapZone.widthAnchor.constraint(equalToConstant: 150), - fullScreenTapZone.heightAnchor.constraint(equalToConstant: 44) - ]) - fullScreenTapZone.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTapNavigationBar))) - navigationItem.titleView = fullScreenTapZone + NotificationCenter.default.addObserver(self, selector: #selector(reloadDueToThemeChange(_:)), name: .CurrentArticleThemeDidChangeNotification, object: nil) articleExtractorButton.addTarget(self, action: #selector(toggleArticleExtractor(_:)), for: .touchUpInside) toolbarItems?.insert(UIBarButtonItem(customView: articleExtractorButton), at: 6) @@ -113,14 +107,14 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:]) pageViewController.delegate = self pageViewController.dataSource = self - + // This code is to disallow paging if we scroll from the left edge. If this code is removed // PoppableGestureRecognizerDelegate will allow us to both navigate back and page back at the // same time. That is really weird when it happens. let panGestureRecognizer = UIPanGestureRecognizer() panGestureRecognizer.delegate = self pageViewController.scrollViewInsidePageControl?.addGestureRecognizer(panGestureRecognizer) - + pageViewController.view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(pageViewController.view) addChild(pageViewController!) @@ -130,7 +124,7 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { view.topAnchor.constraint(equalTo: pageViewController.view.topAnchor), view.bottomAnchor.constraint(equalTo: pageViewController.view.bottomAnchor) ]) - + let controller: WebViewController if let state = restoreState { controller = createWebViewController(article, updateView: false) @@ -145,7 +139,7 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { if let rsp = restoreScrollPosition { controller.setScrollPosition(isShowingExtractedArticle: rsp.isShowingExtractedArticle, articleWindowScrollY: rsp.articleWindowScrollY) } - + articleExtractorButton.buttonState = controller.articleExtractorButtonState self.pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil) @@ -158,6 +152,7 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { searchBar.delegate = self view.bringSubviewToFront(searchBar) + configureAppearanceMenu() updateUI() } @@ -166,10 +161,10 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { if AppDefaults.shared.articleFullscreenEnabled { currentWebViewController?.hideBars() } - + super.viewWillAppear(animated) } - + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) if searchBar != nil && !searchBar.isHidden { @@ -192,14 +187,16 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { readBarButtonItem.isEnabled = false starBarButtonItem.isEnabled = false actionBarButtonItem.isEnabled = false + appearanceBarButtonItem.isEnabled = false return } - + nextUnreadBarButtonItem.isEnabled = coordinator.isAnyUnreadAvailable prevArticleBarButtonItem.isEnabled = coordinator.isPrevArticleAvailable nextArticleBarButtonItem.isEnabled = coordinator.isNextArticleAvailable readBarButtonItem.isEnabled = true starBarButtonItem.isEnabled = true + appearanceBarButtonItem.isEnabled = true let permalinkPresent = article.preferredLink != nil var isFeedProvider = false @@ -233,6 +230,61 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { return currentWebViewController?.webView?.scrollView } + @objc + func configureAppearanceMenu(_ sender: Any? = nil) { + + var themeActions = [UIAction]() + + for themeName in ArticleThemesManager.shared.themeNames { + let action = UIAction(title: themeName, + image: nil, + identifier: nil, + discoverabilityTitle: nil, + attributes: [], + state: ArticleThemesManager.shared.currentThemeName == themeName ? .on : .off, + handler: { action in + ArticleThemesManager.shared.currentThemeName = themeName + }) + themeActions.append(action) + } + + let defaultThemeAction = UIAction(title: NSLocalizedString("Default", comment: "Default"), image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: ArticleThemesManager.shared.currentThemeName == AppDefaults.defaultThemeName ? .on : .off) { _ in + ArticleThemesManager.shared.currentThemeName = AppDefaults.defaultThemeName + } + themeActions.append(defaultThemeAction) + + let themeMenu = UIMenu(title: "Theme", image: AppAssets.themeImage, identifier: nil, options: .singleSelection, children: themeActions) + themeMenu.subtitle = NSLocalizedString("Change the look of articles.", comment: "Change theme") + + var children: [UIMenuElement] = [themeMenu] + + if let currentWebViewController = currentWebViewController { + if currentWebViewController.isFullScreenAvailable { + let fullScreenAction = UIAction(title: NSLocalizedString("Full Screen", comment: "Full Screen"), + image: UIImage(systemName: "arrow.up.backward.and.arrow.down.forward"), + identifier: nil, + discoverabilityTitle: nil, + attributes: [], + state: .off) { [weak self] _ in + self?.currentWebViewController?.hideBars() + } + fullScreenAction.subtitle = NSLocalizedString("Tap the top of the screen to exit Full Screen.", comment: "Exit criteria.") + children.append(fullScreenAction) + } + } + + let appearanceMenu = UIMenu(title: NSLocalizedString("Article Appearance", comment: "Appearance"), image: UIImage(systemName: "textformat.size") , identifier: nil, options: .displayInline, children: children) + + appearanceBarButtonItem.menu = appearanceMenu + + } + + @objc + func reloadDueToThemeChange(_ notification: Notification) { + currentWebViewController?.fullReload() + configureAppearanceMenu() + } + // MARK: Notifications @@ -251,7 +303,7 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { updateUI() } } - + @objc func contentSizeCategoryDidChange(_ note: Notification) { currentWebViewController?.fullReload() } @@ -264,15 +316,11 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { } // MARK: Actions - - @objc func didTapNavigationBar() { - currentWebViewController?.hideBars() - } - + @objc func showBars(_ sender: Any) { currentWebViewController?.showBars() } - + @IBAction func toggleArticleExtractor(_ sender: Any) { currentWebViewController?.toggleArticleExtractor() } @@ -300,35 +348,35 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { @IBAction func showActivityDialog(_ sender: Any) { currentWebViewController?.showActivityDialog(popOverBarButtonItem: actionBarButtonItem) } - + @objc func toggleReaderView(_ sender: Any?) { currentWebViewController?.toggleArticleExtractor() } // MARK: Keyboard Shortcuts - + @objc func navigateToTimeline(_ sender: Any?) { coordinator.navigateToTimeline() } // MARK: API - + func focus() { currentWebViewController?.focus() } - + func canScrollDown() -> Bool { return currentWebViewController?.canScrollDown() ?? false } - + func canScrollUp() -> Bool { return currentWebViewController?.canScrollUp() ?? false } - + func scrollPageDown() { currentWebViewController?.scrollPageDown() } - + func scrollPageUp() { currentWebViewController?.scrollPageUp() } @@ -336,7 +384,7 @@ class ArticleViewController: UIViewController, MainControllerIdentifiable { func stopArticleExtractorIfProcessing() { currentWebViewController?.stopArticleExtractorIfProcessing() } - + func openInAppBrowser() { currentWebViewController?.openInAppBrowser() } @@ -403,9 +451,9 @@ extension ArticleViewController { @objc func keyboardWillChangeFrame(_ notification: Notification) { if !searchBar.isHidden, - let duration = notification.userInfo?[UIWindow.keyboardAnimationDurationUserInfoKey] as? Double, - let curveRaw = notification.userInfo?[UIWindow.keyboardAnimationCurveUserInfoKey] as? UInt, - let frame = notification.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect { + let duration = notification.userInfo?[UIWindow.keyboardAnimationDurationUserInfoKey] as? Double, + let curveRaw = notification.userInfo?[UIWindow.keyboardAnimationCurveUserInfoKey] as? UInt, + let frame = notification.userInfo?[UIWindow.keyboardFrameEndUserInfoKey] as? CGRect { let curve = UIView.AnimationOptions(rawValue: curveRaw) let newHeight = view.safeAreaLayoutGuide.layoutFrame.maxY - frame.minY @@ -438,19 +486,19 @@ extension ArticleViewController: UIPageViewControllerDataSource { func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let webViewController = viewController as? WebViewController, - let currentArticle = webViewController.article, - let article = coordinator.findPrevArticle(currentArticle) else { - return nil - } + let currentArticle = webViewController.article, + let article = coordinator.findPrevArticle(currentArticle) else { + return nil + } return createWebViewController(article) } func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { guard let webViewController = viewController as? WebViewController, - let currentArticle = webViewController.article, - let article = coordinator.findNextArticle(currentArticle) else { - return nil - } + let currentArticle = webViewController.article, + let article = coordinator.findNextArticle(currentArticle) else { + return nil + } return createWebViewController(article) } @@ -459,7 +507,7 @@ extension ArticleViewController: UIPageViewControllerDataSource { // MARK: UIPageViewControllerDelegate 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 } @@ -476,17 +524,17 @@ extension ArticleViewController: UIPageViewControllerDelegate { extension ArticleViewController: UIGestureRecognizerDelegate { - func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - return true - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { + func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return true + } + + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { let point = gestureRecognizer.location(in: nil) if point.x > 40 { return true } return false - } + } } diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index d5c04f8d8..bba8f7432 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -36,8 +36,8 @@ class WebViewController: UIViewController { } private lazy var contextMenuInteraction = UIContextMenuInteraction(delegate: self) - private var isFullScreenAvailable: Bool { - return AppDefaults.shared.articleFullscreenAvailable && traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed + public var isFullScreenAvailable: Bool { + return traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed } private lazy var transition = ImageTransition(controller: self) private var clickedImageCompletion: (() -> Void)? diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index 21e00f0a0..595f0dcf0 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -72,6 +72,11 @@ + + + + + @@ -96,6 +101,7 @@ + @@ -421,6 +427,7 @@ + diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard index 385a825db..d90a425b8 100644 --- a/iOS/Settings/Settings.storyboard +++ b/iOS/Settings/Settings.storyboard @@ -20,14 +20,14 @@ - + - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + -