Implement Read and Star button functionality

This commit is contained in:
Maurice Parker
2020-07-09 18:44:51 -05:00
parent 3e61c7044b
commit 2d57945e98
12 changed files with 166 additions and 154 deletions

View File

@@ -12,11 +12,10 @@ import Articles
struct ArticleContainerView: View {
@EnvironmentObject private var sceneModel: SceneModel
@StateObject private var articleModel = ArticleModel()
var article: Article
@ViewBuilder var body: some View {
ArticleView(sceneModel: sceneModel, articleModel: articleModel, article: article)
ArticleView(sceneModel: sceneModel, article: article)
.modifier(ArticleToolbarModifier())
}

View File

@@ -0,0 +1,14 @@
//
// ArticleManager.swift
// NetNewsWire
//
// Created by Maurice Parker on 7/9/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import Articles
protocol ArticleManager: class {
var currentArticle: Article? { get }
}

View File

@@ -1,68 +0,0 @@
//
// ArticleModel.swift
// NetNewsWire
//
// Created by Maurice Parker on 7/2/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import Foundation
import Foundation
import RSCore
import Account
import Articles
protocol ArticleModelDelegate: class {
var articleModelWebViewProvider: WebViewProvider? { get }
func findPrevArticle(_: ArticleModel, article: Article) -> Article?
func findNextArticle(_: ArticleModel, article: Article) -> Article?
func selectArticle(_: ArticleModel, article: Article)
}
protocol ArticleManager: class {
var currentArticle: Article? { get }
}
class ArticleModel: ObservableObject {
weak var articleManager: ArticleManager?
weak var delegate: ArticleModelDelegate?
var webViewProvider: WebViewProvider? {
return delegate?.articleModelWebViewProvider
}
var currentArticle: Article? {
return articleManager?.currentArticle
}
// MARK: API
func findPrevArticle(_ article: Article) -> Article? {
return delegate?.findPrevArticle(self, article: article)
}
func findNextArticle(_ article: Article) -> Article? {
return delegate?.findNextArticle(self, article: article)
}
func selectArticle(_ article: Article) {
delegate?.selectArticle(self, article: article)
}
func toggleReadForCurrentArticle() {
if let article = currentArticle {
markArticles([article], statusKey: .starred, flag: !article.status.starred)
}
}
func toggleStarForCurrentArticle() {
if let article = currentArticle {
markArticles([article], statusKey: .starred, flag: !article.status.starred)
}
}
}

View File

@@ -10,6 +10,8 @@ import SwiftUI
struct ArticleToolbarModifier: ViewModifier {
@EnvironmentObject private var sceneModel: SceneModel
func body(content: Content) -> some View {
content
.toolbar {
@@ -31,11 +33,15 @@ struct ArticleToolbarModifier: ViewModifier {
}
ToolbarItem(placement: .bottomBar) {
Button(action: {
}, label: {
AppAssets.readOpenImage
.font(.title3)
}).help("Mark as Unread")
Button(action: { sceneModel.toggleReadForCurrentArticle() }, label: {
if sceneModel.readButtonState == .on {
AppAssets.readClosedImage
} else {
AppAssets.readOpenImage
}
})
.disabled(sceneModel.readButtonState == nil ? true : false)
.help(sceneModel.readButtonState == .on ? "Mark as Unread" : "Mark as Read")
}
ToolbarItem(placement: .bottomBar) {
@@ -43,11 +49,15 @@ struct ArticleToolbarModifier: ViewModifier {
}
ToolbarItem(placement: .bottomBar) {
Button(action: {
}, label: {
AppAssets.starOpenImage
.font(.title3)
}).help("Mark as Starred")
Button(action: { sceneModel.toggleStarForCurrentArticle() }, label: {
if sceneModel.starButtonState == .on {
AppAssets.starClosedImage
} else {
AppAssets.starOpenImage
}
})
.disabled(sceneModel.starButtonState == nil ? true : false)
.help(sceneModel.starButtonState == .on ? "Mark as Unstarred" : "Mark as Starred")
}
ToolbarItem(placement: .bottomBar) {

View File

@@ -14,49 +14,90 @@ import RSCore
final class SceneModel: ObservableObject {
@Published var refreshProgressState = RefreshProgressModel.State.none
@Published var readButtonState: ArticleReadButtonState?
@Published var starButtonState: ArticleStarButtonState?
private var refreshProgressModel: RefreshProgressModel? = nil
private var articleIconSchemeHandler: ArticleIconSchemeHandler? = nil
var webViewProvider: WebViewProvider? = nil
var undoManager: UndoManager?
var undoableCommands = [UndoableCommand]()
var sidebarModel: SidebarModel?
var timelineModel: TimelineModel?
var articleModel: ArticleModel?
private var refreshProgressModel: RefreshProgressModel? = nil
private var articleIconSchemeHandler: ArticleIconSchemeHandler? = nil
private var webViewProvider: WebViewProvider? = nil
var articleManager: ArticleManager?
var currentArticle: Article? {
return articleManager?.currentArticle
}
// MARK: Initialization API
/// Prepares the SceneModel to be used in the views
func startup() {
self.refreshProgressModel = RefreshProgressModel()
self.refreshProgressModel!.$state.assign(to: self.$refreshProgressState)
self.articleIconSchemeHandler = ArticleIconSchemeHandler(sceneModel: self)
self.webViewProvider = WebViewProvider(articleIconSchemeHandler: self.articleIconSchemeHandler!)
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
}
// MARK: Article Status Change API
// MARK: Article Management API
/// Toggles the read indicator for the currently viewable article
func toggleReadForCurrentArticle() {
articleModel?.toggleReadForCurrentArticle()
if let article = articleManager?.currentArticle {
toggleRead(article)
}
}
/// Toggles the read indicator for the given article
func toggleRead(_ article: Article) {
guard !article.status.read || article.isAvailableToMarkUnread else { return }
markArticles([article], statusKey: .read, flag: !article.status.read)
}
/// Toggles the star indicator for the currently viewable article
func toggleStarForCurrentArticle() {
articleModel?.toggleStarForCurrentArticle()
if let article = articleManager?.currentArticle {
toggleStar(article)
}
}
/// Toggles the star indicator for the given article
func toggleStar(_ article: Article) {
markArticles([article], statusKey: .starred, flag: !article.status.starred)
}
// MARK: Resource lookup API
/// Retrieves the article before the given article in the Timeline
func findPrevArticle(_ article: Article) -> Article? {
return timelineModel?.findPrevArticle(article)
}
/// Retrieves the article after the given article in the Timeline
func findNextArticle(_ article: Article) -> Article? {
return timelineModel?.findNextArticle(article)
}
/// Marks the article as read and selects it in the Timeline. Don't call until after the ArticleManager article has been set.
func updateArticleSelection() {
guard let article = currentArticle else { return }
timelineModel?.selectArticle(article)
if article.status.read {
updateArticleState()
} else {
markArticles([article], statusKey: .read, flag: true)
}
}
/// Returns the article with the given articleID
func articleFor(_ articleID: String) -> Article? {
return timelineModel?.articleFor(articleID)
}
@@ -83,28 +124,6 @@ extension SceneModel: TimelineModelDelegate {
}
// MARK: ArticleModelDelegate
extension SceneModel: ArticleModelDelegate {
var articleModelWebViewProvider: WebViewProvider? {
return webViewProvider
}
func findPrevArticle(_: ArticleModel, article: Article) -> Article? {
return timelineModel?.findPrevArticle(article)
}
func findNextArticle(_: ArticleModel, article: Article) -> Article? {
return timelineModel?.findNextArticle(article)
}
func selectArticle(_: ArticleModel, article: Article) {
timelineModel?.selectArticle(article)
}
}
// MARK: UndoableCommandRunner
extension SceneModel: UndoableCommandRunner {
@@ -122,5 +141,33 @@ extension SceneModel: UndoableCommandRunner {
private extension SceneModel {
// MARK: Notifications
@objc func statusesDidChange(_ note: Notification) {
guard let article = currentArticle, let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set<String> else {
return
}
if articleIDs.contains(article.articleID) {
updateArticleState()
}
}
// MARK: State Updates
func updateArticleState() {
guard let article = currentArticle else {
readButtonState = nil
starButtonState = nil
return
}
if article.isAvailableToMarkUnread {
readButtonState = article.status.read ? .off : .on
} else {
readButtonState = nil
}
starButtonState = article.status.starred ? .on : .off
}
}

View File

@@ -100,14 +100,26 @@ struct SceneNavigationView: View {
}).help("Go to Next Unread").padding(.trailing, 40)
}
ToolbarItem {
Button(action: {}, label: {
AppAssets.starOpenImage
}).help("Mark as Starred")
Button(action: { sceneModel.toggleReadForCurrentArticle() }, label: {
if sceneModel.readButtonState == .on {
AppAssets.readClosedImage
} else {
AppAssets.readOpenImage
}
})
.disabled(sceneModel.readButtonState == nil ? true : false)
.help(sceneModel.readButtonState == .on ? "Mark as Unread" : "Mark as Read")
}
ToolbarItem {
Button(action: {}, label: {
AppAssets.readClosedImage
}).help("Mark as Unread")
Button(action: { sceneModel.toggleStarForCurrentArticle() }, label: {
if sceneModel.starButtonState == .on {
AppAssets.starClosedImage
} else {
AppAssets.starOpenImage
}
})
.disabled(sceneModel.starButtonState == nil ? true : false)
.help(sceneModel.starButtonState == .on ? "Mark as Unstarred" : "Mark as Starred")
}
ToolbarItem {
Button(action: {}, label: {

View File

@@ -12,21 +12,18 @@ import Articles
final class ArticleView: UIViewControllerRepresentable {
var sceneModel: SceneModel
var articleModel: ArticleModel
var article: Article
init(sceneModel: SceneModel, articleModel: ArticleModel, article: Article) {
init(sceneModel: SceneModel, article: Article) {
self.sceneModel = sceneModel
self.articleModel = articleModel
self.article = article
sceneModel.articleModel = articleModel
articleModel.delegate = sceneModel
}
func makeUIViewController(context: Context) -> ArticleViewController {
let controller = ArticleViewController()
controller.articleModel = articleModel
controller.article = article
sceneModel.articleManager = controller
controller.sceneModel = sceneModel
controller.currentArticle = article
return controller
}

View File

@@ -14,7 +14,7 @@ import SafariServices
class ArticleViewController: UIViewController, ArticleManager {
weak var articleModel: ArticleModel?
weak var sceneModel: SceneModel?
private var pageViewController: UIPageViewController!
@@ -24,8 +24,8 @@ class ArticleViewController: UIViewController, ArticleManager {
var currentArticle: Article? {
didSet {
if let controller = currentWebViewController, controller.article != article {
controller.setArticle(article)
if let controller = currentWebViewController, controller.article != currentArticle {
controller.setArticle(currentArticle)
DispatchQueue.main.async {
// You have to set the view controller to clear out the UIPageViewController child controller cache.
// You also have to do it in an async call or you will get a strange assertion error.
@@ -52,9 +52,10 @@ class ArticleViewController: UIViewController, ArticleManager {
view.bottomAnchor.constraint(equalTo: pageViewController.view.bottomAnchor)
])
let controller = createWebViewController(article, updateView: true)
let controller = createWebViewController(currentArticle, updateView: true)
self.pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil)
sceneModel?.updateArticleSelection()
}
// MARK: API
@@ -97,7 +98,7 @@ extension ArticleViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard let webViewController = viewController as? WebViewController,
let currentArticle = webViewController.article,
let article = articleModel?.findPrevArticle(currentArticle) else {
let article = sceneModel?.findPrevArticle(currentArticle) else {
return nil
}
return createWebViewController(article)
@@ -106,7 +107,7 @@ extension ArticleViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard let webViewController = viewController as? WebViewController,
let currentArticle = webViewController.article,
let article = articleModel?.findNextArticle(currentArticle) else {
let article = sceneModel?.findNextArticle(currentArticle) else {
return nil
}
return createWebViewController(article)
@@ -120,9 +121,9 @@ 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 }
// guard let article = currentWebViewController?.article else { return }
articleModel?.selectArticle(article)
sceneModel?.updateArticleSelection()
// articleExtractorButton.buttonState = currentWebViewController?.articleExtractorButtonState ?? .off
previousViewControllers.compactMap({ $0 as? WebViewController }).forEach({ $0.stopWebViewActivity() })
@@ -136,15 +137,15 @@ private extension ArticleViewController {
func createWebViewController(_ article: Article?, updateView: Bool = true) -> WebViewController {
let controller = WebViewController()
controller.articleModel = articleModel
controller.sceneModel = sceneModel
controller.delegate = self
controller.setArticle(article, updateView: updateView)
return controller
}
func resetWebViewController() {
articleModel?.webViewProvider?.flushQueue()
articleModel?.webViewProvider?.replenishQueueIfNeeded()
sceneModel?.webViewProvider?.flushQueue()
sceneModel?.webViewProvider?.replenishQueueIfNeeded()
if let controller = currentWebViewController {
controller.fullReload()
self.pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil)

View File

@@ -57,7 +57,7 @@ class WebViewController: UIViewController {
}
}
var articleModel: ArticleModel?
var sceneModel: SceneModel?
weak var delegate: WebViewControllerDelegate?
private(set) var article: Article?
@@ -372,9 +372,10 @@ extension WebViewController: WKScriptMessageHandler {
case MessageName.imageWasClicked:
imageWasClicked(body: message.body as? String)
case MessageName.showFeedInspector:
if let webFeed = article?.webFeed {
return
// if let webFeed = article?.webFeed {
// coordinator.showFeedInspector(for: webFeed)
}
// }
default:
return
}
@@ -449,7 +450,7 @@ private extension WebViewController {
return
}
articleModel?.webViewProvider?.dequeueWebView() { webView in
sceneModel?.webViewProvider?.dequeueWebView() { webView in
// Add the webview
webView.translatesAutoresizingMaskIntoConstraints = false

View File

@@ -12,20 +12,17 @@ import Articles
struct ArticleView: NSViewControllerRepresentable {
var sceneModel: SceneModel
var articleModel: ArticleModel
var article: Article
init(sceneModel: SceneModel, articleModel: ArticleModel, article: Article) {
init(sceneModel: SceneModel, article: Article) {
self.sceneModel = sceneModel
self.articleModel = articleModel
self.article = article
sceneModel.articleModel = articleModel
articleModel.delegate = sceneModel
}
func makeNSViewController(context: Context) -> WebViewController {
let controller = WebViewController()
controller.articleModel = articleModel
sceneModel.articleManager = controller
controller.sceneModel = sceneModel
controller.currentArticle = article
return controller
}

View File

@@ -46,7 +46,7 @@ class WebViewController: NSViewController, ArticleManager {
}
}
var articleModel: ArticleModel?
var sceneModel: SceneModel?
weak var delegate: WebViewControllerDelegate?
var currentArticle: Article?
@@ -75,6 +75,8 @@ class WebViewController: NSViewController, ArticleManager {
])
loadWebView()
sceneModel?.updateArticleSelection()
}
// MARK: Notifications
@@ -217,7 +219,7 @@ private extension WebViewController {
return
}
articleModel?.webViewProvider?.dequeueWebView() { webView in
sceneModel?.webViewProvider?.dequeueWebView() { webView in
// Add the webview
webView.translatesAutoresizingMaskIntoConstraints = false