// // SceneModel.swift // NetNewsWire // // Created by Maurice Parker on 6/28/20. // Copyright © 2020 Ranchero Software. All rights reserved. // import Foundation import Combine import Account import Articles import RSCore final class SceneModel: ObservableObject { @Published var markAllAsReadButtonState: Bool? @Published var nextUnreadButtonState: Bool? @Published var readButtonState: Bool? @Published var starButtonState: Bool? @Published var extractorButtonState: ArticleExtractorButtonState? @Published var openInBrowserButtonState: Bool? @Published var shareButtonState: Bool? @Published var accountSyncErrors: [AccountSyncError] = [] var selectedArticles: [Article] { timelineModel.selectedArticles } private var refreshProgressModel: RefreshProgressModel? = nil private var articleIconSchemeHandler: ArticleIconSchemeHandler? = nil private(set) var webViewProvider: WebViewProvider? = nil private(set) lazy var sidebarModel = SidebarModel(delegate: self) private(set) lazy var timelineModel = TimelineModel(delegate: self) private var cancellables = Set() // MARK: Initialization API /// Prepares the SceneModel to be used in the views func startup() { self.articleIconSchemeHandler = ArticleIconSchemeHandler(sceneModel: self) self.webViewProvider = WebViewProvider(articleIconSchemeHandler: self.articleIconSchemeHandler!) subscribeToAccountSyncErrors() subscribeToToolbarChangeEvents() } // MARK: Navigation API /// Goes to the next unread item found in Sidebar and Timeline order, top to bottom func goToNextUnread() { if !timelineModel.goToNextUnread() { timelineModel.selectNextUnreadSubject.send(true) sidebarModel.selectNextUnread.send() } } // MARK: Article Management API /// Marks all the articles in the Timeline as read func markAllAsRead() { timelineModel.markAllAsReadSubject.send() } /// Toggles the read status for the selected articles func toggleReadStatusForSelectedArticles() { timelineModel.toggleReadStatusForSelectedArticlesSubject.send() } /// Toggles the star status for the selected articles func toggleStarredStatusForSelectedArticles() { timelineModel.toggleStarredStatusForSelectedArticlesSubject.send() } /// Opens the selected article in an external browser func openSelectedArticleInBrowser() { timelineModel.openSelectedArticlesInBrowserSubject.send() } /// 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) } /// Returns the article with the given articleID func articleFor(_ articleID: String) -> Article? { return timelineModel.articleFor(articleID) } } // MARK: SidebarModelDelegate extension SceneModel: SidebarModelDelegate { func unreadCount(for feed: Feed) -> Int { // TODO: Get the count from the timeline if Feed is the current timeline return feed.unreadCount } } // MARK: TimelineModelDelegate extension SceneModel: TimelineModelDelegate { var selectedFeedsPublisher: AnyPublisher<[Feed], Never>? { return sidebarModel.selectedFeedsPublisher } func timelineRequestedWebFeedSelection(_: TimelineModel, webFeed: WebFeed) { } } // MARK: Private private extension SceneModel { // MARK: Subscriptions func subscribeToToolbarChangeEvents() { guard let selectedArticlesPublisher = timelineModel.selectedArticlesPublisher else { return } NotificationCenter.default.publisher(for: .UnreadCountDidChange) .compactMap { $0.object as? AccountManager } .sink { [weak self] accountManager in self?.updateNextUnreadButtonState(accountManager: accountManager) }.store(in: &cancellables) let blankNotification = Notification(name: .StatusesDidChange) let statusesDidChangePublisher = NotificationCenter.default.publisher(for: .StatusesDidChange).prepend(blankNotification) statusesDidChangePublisher .combineLatest(selectedArticlesPublisher) .sink { [weak self] _, selectedArticles in self?.updateArticleButtonsState(selectedArticles: selectedArticles) } .store(in: &cancellables) statusesDidChangePublisher .combineLatest(timelineModel.articlesSubject) .sink { [weak self] _, articles in self?.updateMarkAllAsReadButtonsState(articles: articles) } .store(in: &cancellables) } func subscribeToAccountSyncErrors() { NotificationCenter.default.publisher(for: .AccountsDidFailToSyncWithErrors) .sink { [weak self] notification in guard let syncErrors = notification.userInfo?[Account.UserInfoKey.syncErrors] as? [AccountSyncError] else { return } self?.accountSyncErrors = syncErrors }.store(in: &cancellables) } // MARK: Button State Updates func updateNextUnreadButtonState(accountManager: AccountManager) { if accountManager.unreadCount > 0 { self.nextUnreadButtonState = false } else { self.nextUnreadButtonState = nil } } func updateMarkAllAsReadButtonsState(articles: [Article]) { if articles.canMarkAllAsRead() { markAllAsReadButtonState = false } else { markAllAsReadButtonState = nil } } func updateArticleButtonsState(selectedArticles: [Article]) { guard !selectedArticles.isEmpty else { readButtonState = nil starButtonState = nil openInBrowserButtonState = nil shareButtonState = nil return } if selectedArticles.anyArticleIsUnread() { readButtonState = true } else if selectedArticles.anyArticleIsReadAndCanMarkUnread() { readButtonState = false } else { readButtonState = nil } if selectedArticles.anyArticleIsUnstarred() { starButtonState = false } else { starButtonState = true } if selectedArticles.count == 1, selectedArticles.first?.preferredLink != nil { openInBrowserButtonState = true shareButtonState = true } else { openInBrowserButtonState = nil shareButtonState = nil } } }