From 2d57945e9822a7573e1b184bb44622d76c432ace Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 9 Jul 2020 18:44:51 -0500 Subject: [PATCH] Implement Read and Star button functionality --- .../Shared/Article/ArticleContainerView.swift | 3 +- .../Shared/Article/ArticleManager.swift | 14 +++ .../Shared/Article/ArticleModel.swift | 68 ----------- .../Article/ArticleToolbarModifier.swift | 30 +++-- Multiplatform/Shared/SceneModel.swift | 111 +++++++++++++----- .../Shared/SceneNavigationView.swift | 24 +++- Multiplatform/iOS/Article/ArticleView.swift | 11 +- .../iOS/Article/ArticleViewController.swift | 23 ++-- .../iOS/Article/WebViewController.swift | 9 +- Multiplatform/macOS/Article/ArticleView.swift | 9 +- .../macOS/Article/WebViewController.swift | 6 +- NetNewsWire.xcodeproj/project.pbxproj | 12 +- 12 files changed, 166 insertions(+), 154 deletions(-) create mode 100644 Multiplatform/Shared/Article/ArticleManager.swift delete mode 100644 Multiplatform/Shared/Article/ArticleModel.swift diff --git a/Multiplatform/Shared/Article/ArticleContainerView.swift b/Multiplatform/Shared/Article/ArticleContainerView.swift index dbf6820b6..3d1a8d225 100644 --- a/Multiplatform/Shared/Article/ArticleContainerView.swift +++ b/Multiplatform/Shared/Article/ArticleContainerView.swift @@ -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()) } diff --git a/Multiplatform/Shared/Article/ArticleManager.swift b/Multiplatform/Shared/Article/ArticleManager.swift new file mode 100644 index 000000000..adcc9e10e --- /dev/null +++ b/Multiplatform/Shared/Article/ArticleManager.swift @@ -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 } +} diff --git a/Multiplatform/Shared/Article/ArticleModel.swift b/Multiplatform/Shared/Article/ArticleModel.swift deleted file mode 100644 index befd31676..000000000 --- a/Multiplatform/Shared/Article/ArticleModel.swift +++ /dev/null @@ -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) - } - } - - -} - diff --git a/Multiplatform/Shared/Article/ArticleToolbarModifier.swift b/Multiplatform/Shared/Article/ArticleToolbarModifier.swift index 65b18a854..8fddef334 100644 --- a/Multiplatform/Shared/Article/ArticleToolbarModifier.swift +++ b/Multiplatform/Shared/Article/ArticleToolbarModifier.swift @@ -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) { diff --git a/Multiplatform/Shared/SceneModel.swift b/Multiplatform/Shared/SceneModel.swift index 9063c5bc0..b81c23d57 100644 --- a/Multiplatform/Shared/SceneModel.swift +++ b/Multiplatform/Shared/SceneModel.swift @@ -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 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 + } } diff --git a/Multiplatform/Shared/SceneNavigationView.swift b/Multiplatform/Shared/SceneNavigationView.swift index c39964e5e..02c6de558 100644 --- a/Multiplatform/Shared/SceneNavigationView.swift +++ b/Multiplatform/Shared/SceneNavigationView.swift @@ -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: { diff --git a/Multiplatform/iOS/Article/ArticleView.swift b/Multiplatform/iOS/Article/ArticleView.swift index d7c5b85da..989659371 100644 --- a/Multiplatform/iOS/Article/ArticleView.swift +++ b/Multiplatform/iOS/Article/ArticleView.swift @@ -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 } diff --git a/Multiplatform/iOS/Article/ArticleViewController.swift b/Multiplatform/iOS/Article/ArticleViewController.swift index ae0a8d175..dc51acc3d 100644 --- a/Multiplatform/iOS/Article/ArticleViewController.swift +++ b/Multiplatform/iOS/Article/ArticleViewController.swift @@ -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) diff --git a/Multiplatform/iOS/Article/WebViewController.swift b/Multiplatform/iOS/Article/WebViewController.swift index 794b07418..1630124bd 100644 --- a/Multiplatform/iOS/Article/WebViewController.swift +++ b/Multiplatform/iOS/Article/WebViewController.swift @@ -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 diff --git a/Multiplatform/macOS/Article/ArticleView.swift b/Multiplatform/macOS/Article/ArticleView.swift index abd4a17b8..51b5f16ef 100644 --- a/Multiplatform/macOS/Article/ArticleView.swift +++ b/Multiplatform/macOS/Article/ArticleView.swift @@ -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 } diff --git a/Multiplatform/macOS/Article/WebViewController.swift b/Multiplatform/macOS/Article/WebViewController.swift index 3bf93c802..8150e3d4a 100644 --- a/Multiplatform/macOS/Article/WebViewController.swift +++ b/Multiplatform/macOS/Article/WebViewController.swift @@ -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 diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 9a5aa1a5e..a84a81401 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -211,6 +211,8 @@ 51707439232AA97100A461A3 /* ShareFolderPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51707438232AA97100A461A3 /* ShareFolderPickerController.swift */; }; 5171B4D424B7BABA00FB8D3B /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; }; 5171B4F624B7BABA00FB8D3B /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; }; + 5171B4F824B7CB3600FB8D3B /* ArticleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5171B4F724B7CB3600FB8D3B /* ArticleManager.swift */; }; + 5171B4F924B7CB3600FB8D3B /* ArticleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5171B4F724B7CB3600FB8D3B /* ArticleManager.swift */; }; 517630042336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; }; 517630052336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; }; 517630232336657E00E15FFF /* WebViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517630222336657E00E15FFF /* WebViewProvider.swift */; }; @@ -312,8 +314,6 @@ 51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */; }; 51A5769624AE617200078888 /* ArticleContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A5769524AE617200078888 /* ArticleContainerView.swift */; }; 51A5769724AE617200078888 /* ArticleContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A5769524AE617200078888 /* ArticleContainerView.swift */; }; - 51A576BB24AE621800078888 /* ArticleModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A576BA24AE621800078888 /* ArticleModel.swift */; }; - 51A576BC24AE621800078888 /* ArticleModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A576BA24AE621800078888 /* ArticleModel.swift */; }; 51A66685238075AE00CB272D /* AddWebFeedDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */; }; 51A9A5E12380C4FE0033AADF /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45255226507D200C03939 /* AppDefaults.swift */; }; 51A9A5E42380C8880033AADF /* ShareFolderPickerAccountCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */; }; @@ -1906,6 +1906,7 @@ 516AE9B22371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedTableViewSectionHeaderLayout.swift; sourceTree = ""; }; 516AE9DE2372269A007DEEAA /* IconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImage.swift; sourceTree = ""; }; 51707438232AA97100A461A3 /* ShareFolderPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareFolderPickerController.swift; sourceTree = ""; }; + 5171B4F724B7CB3600FB8D3B /* ArticleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleManager.swift; sourceTree = ""; }; 517630032336215100E15FFF /* main.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = main.js; sourceTree = ""; }; 517630222336657E00E15FFF /* WebViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewProvider.swift; sourceTree = ""; }; 5177470224B2657F00EB0F74 /* TimelineToolbarModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineToolbarModifier.swift; sourceTree = ""; }; @@ -1976,7 +1977,6 @@ 51A16995235E10D600EB091F /* AboutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbinAccountViewController.swift; sourceTree = ""; }; 51A5769524AE617200078888 /* ArticleContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleContainerView.swift; sourceTree = ""; }; - 51A576BA24AE621800078888 /* ArticleModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleModel.swift; sourceTree = ""; }; 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedDefaultContainer.swift; sourceTree = ""; }; 51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareFolderPickerAccountCell.xib; sourceTree = ""; }; 51A9A5E52380C8B20033AADF /* ShareFolderPickerFolderCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareFolderPickerFolderCell.xib; sourceTree = ""; }; @@ -2863,11 +2863,11 @@ 5177472124B38CAE00EB0F74 /* ArticleButtonState.swift */, 51A5769524AE617200078888 /* ArticleContainerView.swift */, 5177471524B37D9700EB0F74 /* ArticleIconSchemeHandler.swift */, - 51A576BA24AE621800078888 /* ArticleModel.swift */, 5177470524B2910300EB0F74 /* ArticleToolbarModifier.swift */, 5177471324B37D4000EB0F74 /* PreloadedWebView.swift */, 5177471924B3863000EB0F74 /* WebViewProvider.swift */, 517B2EBB24B3E62A001AC46C /* WrapperScriptMessageHandler.swift */, + 5171B4F724B7CB3600FB8D3B /* ArticleManager.swift */, ); path = Article; sourceTree = ""; @@ -5085,7 +5085,6 @@ 51E4993C24A8709900B667CB /* AppDelegate.swift in Sources */, 6591727F24B5D19500B638E8 /* SettingsDetailAccountView.swift in Sources */, 51E498F924A8085D00B667CB /* SmartFeed.swift in Sources */, - 51A576BB24AE621800078888 /* ArticleModel.swift in Sources */, 65ACE48824B48020003AE06A /* SettingsLocalAccountView.swift in Sources */, 17930ED424AF10EE00A9BA52 /* AddWebFeedView.swift in Sources */, 51E4995124A8734D00B667CB /* ExtensionPointManager.swift in Sources */, @@ -5096,6 +5095,7 @@ 51E4990324A808BB00B667CB /* FaviconDownloader.swift in Sources */, 172199ED24AB2E0100A31D04 /* SafariView.swift in Sources */, 65ACE48624B477C9003AE06A /* SettingsAccountLabelView.swift in Sources */, + 5171B4F824B7CB3600FB8D3B /* ArticleManager.swift in Sources */, 51E4990224A808BB00B667CB /* ColorHash.swift in Sources */, 51919FAC24AA8CCA00541E64 /* UnreadCountView.swift in Sources */, 5177476224B3BC4700EB0F74 /* SettingsAboutView.swift in Sources */, @@ -5142,6 +5142,7 @@ 51E4996C24A8762D00B667CB /* ExtractedArticle.swift in Sources */, 51E4990824A808C300B667CB /* RSHTMLMetadata+Extension.swift in Sources */, 51919FF824AB8B7700541E64 /* TimelineView.swift in Sources */, + 5171B4F924B7CB3600FB8D3B /* ArticleManager.swift in Sources */, 51E4992B24A8676300B667CB /* ArticleArray.swift in Sources */, 51B54AB324B5AC830014348B /* ArticleButtonState.swift in Sources */, 17D5F17224B0BC6700375168 /* SidebarToolbarModel.swift in Sources */, @@ -5180,7 +5181,6 @@ 51919FB424AAB97900541E64 /* FeedIconImageLoader.swift in Sources */, 51E4994A24A8734C00B667CB /* ExtensionPointManager.swift in Sources */, 514E6C0324AD29A300AC6F6E /* TimelineItemStatusView.swift in Sources */, - 51A576BC24AE621800078888 /* ArticleModel.swift in Sources */, 51B54A6524B549B20014348B /* WrapperScriptMessageHandler.swift in Sources */, 51E4996D24A8762D00B667CB /* ArticleExtractor.swift in Sources */, 51E4994024A8713B00B667CB /* AccountRefreshTimer.swift in Sources */,