mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Implement Read and Star button functionality
This commit is contained in:
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
14
Multiplatform/Shared/Article/ArticleManager.swift
Normal file
14
Multiplatform/Shared/Article/ArticleManager.swift
Normal 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 }
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user