diff --git a/Multiplatform/Shared/AppAssets.swift b/Multiplatform/Shared/AppAssets.swift index 37998ed7b..b6930bb34 100644 --- a/Multiplatform/Shared/AppAssets.swift +++ b/Multiplatform/Shared/AppAssets.swift @@ -104,6 +104,16 @@ struct AppAssets { #endif }() + static var timelineStarred: Image = { + return Image(systemName: "star.fill") + + }() + + static var timelineUnread: Image = { + return Image(systemName: "circle.fill") + + }() + static var todayFeedImage: IconImage = { #if os(macOS) return IconImage(NSImage(systemSymbolName: "sun.max.fill", accessibilityDescription: nil)!) diff --git a/Multiplatform/Shared/Images/ArticleIconImageLoader.swift b/Multiplatform/Shared/Images/ArticleIconImageLoader.swift new file mode 100644 index 000000000..ec08f5189 --- /dev/null +++ b/Multiplatform/Shared/Images/ArticleIconImageLoader.swift @@ -0,0 +1,59 @@ +// +// ArticleIconImageLoader.swift +// NetNewsWire +// +// Created by Maurice Parker on 7/1/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import Foundation +import Account +import Articles + +final class ArticleIconImageLoader: ObservableObject { + + private var article: Article? + + @Published var image: IconImage? + + init() { + NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil) + } + + func loadImage(for article: Article) { + guard image == nil else { return } + self.article = article + image = article.iconImage() + } + +} + +private extension ArticleIconImageLoader { + + @objc func faviconDidBecomeAvailable(_ note: Notification) { + guard let article = article else { return } + image = article.iconImage() + } + + @objc func webFeedIconDidBecomeAvailable(_ note: Notification) { + guard let article = article, let noteFeed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed, noteFeed == article.webFeed else { + return + } + image = article.iconImage() + } + + @objc func avatarDidBecomeAvailable(_ note: Notification) { + guard let article = article, let authors = article.authors, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else { + return + } + + for author in authors { + if author.avatarURL == avatarURL { + image = article.iconImage() + return + } + } + } +} diff --git a/Multiplatform/Shared/Images/FeedImageLoader.swift b/Multiplatform/Shared/Images/FeedIconImageLoader.swift similarity index 84% rename from Multiplatform/Shared/Images/FeedImageLoader.swift rename to Multiplatform/Shared/Images/FeedIconImageLoader.swift index 5dc6a40c7..6a6fa8586 100644 --- a/Multiplatform/Shared/Images/FeedImageLoader.swift +++ b/Multiplatform/Shared/Images/FeedIconImageLoader.swift @@ -1,5 +1,5 @@ // -// FeedImageLoader.swift +// FeedIconImageLoader.swift // NetNewsWire // // Created by Maurice Parker on 6/29/20. @@ -9,7 +9,7 @@ import SwiftUI import Account -final class FeedImageLoader: ObservableObject { +final class FeedIconImageLoader: ObservableObject { private var feed: Feed? @@ -21,8 +21,27 @@ final class FeedImageLoader: ObservableObject { } func loadImage(for feed: Feed) { + guard image == nil else { return } self.feed = feed - + fetchImage() + } + +} + +private extension FeedIconImageLoader { + + @objc func faviconDidBecomeAvailable(_ note: Notification) { + fetchImage() + } + + @objc func webFeedIconDidBecomeAvailable(_ note: Notification) { + guard let feed = feed as? WebFeed, let noteFeed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed, feed == noteFeed else { + return + } + fetchImage() + } + + func fetchImage() { if let webFeed = feed as? WebFeed { if let feedIconImage = appDelegate.webFeedIconDownloader.icon(for: webFeed) { image = feedIconImage @@ -40,19 +59,3 @@ final class FeedImageLoader: ObservableObject { } } - -private extension FeedImageLoader { - - @objc func faviconDidBecomeAvailable(_ note: Notification) { - guard let feed = feed else { return } - loadImage(for: feed) - } - - @objc func webFeedIconDidBecomeAvailable(_ note: Notification) { - guard let feed = feed as? WebFeed, let noteFeed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed, feed == noteFeed else { - return - } - loadImage(for: feed) - } - -} diff --git a/Multiplatform/Shared/Images/IconImageView.swift b/Multiplatform/Shared/Images/IconImageView.swift index 3eb74a1ca..95a187518 100644 --- a/Multiplatform/Shared/Images/IconImageView.swift +++ b/Multiplatform/Shared/Images/IconImageView.swift @@ -13,20 +13,10 @@ struct IconImageView: View { var iconImage: IconImage var body: some View { - #if os(macOS) - return Image(nsImage: iconImage.image) + return Image(rsImage: iconImage.image) .resizable() .scaledToFit() - .frame(width: 20, height: 20, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) .cornerRadius(4) - #endif - #if os(iOS) - return Image(uiImage: iconImage.image) - .resizable() - .scaledToFit() - .frame(width: 20, height: 20, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) - .cornerRadius(4) - #endif } } diff --git a/Multiplatform/Shared/Previews/PreviewArticles.swift b/Multiplatform/Shared/Previews/PreviewArticles.swift new file mode 100644 index 000000000..75cc895d9 --- /dev/null +++ b/Multiplatform/Shared/Previews/PreviewArticles.swift @@ -0,0 +1,58 @@ +// +// PreviewArticles.swift +// NetNewsWire +// +// Created by Maurice Parker on 7/1/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import Foundation +import Articles + +enum PreviewArticles { + + static var basicUnread: Article { + return makeBasicArticle(read: false, starred: false) + } + + static var basicRead: Article { + return makeBasicArticle(read: true, starred: false) + } + + static var basicStarred: Article { + return makeBasicArticle(read: false, starred: true) + } + +} + +private extension PreviewArticles { + + static var shortTitle: String { + return "Short article title" + } + + static var shortSummary: String { + return "Summary of article to be shown after title." + } + + static func makeBasicArticle(read: Bool, starred: Bool) -> Article { + let articleID = "prototype" + let status = ArticleStatus(articleID: articleID, read: read, starred: starred, dateArrived: Date()) + return Article(accountID: articleID, + articleID: articleID, + webFeedID: articleID, + uniqueID: articleID, + title: shortTitle, + contentHTML: nil, + contentText: nil, + url: nil, + externalURL: nil, + summary: shortSummary, + imageURL: nil, + datePublished: Date(), + dateModified: nil, + authors: nil, + status: status) + } + +} diff --git a/Multiplatform/Shared/SceneModel.swift b/Multiplatform/Shared/SceneModel.swift index 033b56148..f09b48f6d 100644 --- a/Multiplatform/Shared/SceneModel.swift +++ b/Multiplatform/Shared/SceneModel.swift @@ -20,10 +20,6 @@ final class SceneModel: ObservableObject { extension SceneModel: SidebarModelDelegate { - func sidebarSelectionDidChange(_: SidebarModel, feeds: [Feed]?) { - print("**** sidebar selection changed ***") - } - func unreadCount(for feed: Feed) -> Int { // TODO: Get the count from the timeline if Feed is the current timeline return feed.unreadCount diff --git a/Multiplatform/Shared/Sidebar/SidebarExpandedContainers.swift b/Multiplatform/Shared/Sidebar/SidebarExpandedContainers.swift index 190135cde..ebccb6034 100644 --- a/Multiplatform/Shared/Sidebar/SidebarExpandedContainers.swift +++ b/Multiplatform/Shared/Sidebar/SidebarExpandedContainers.swift @@ -29,11 +29,7 @@ final class SidebarExpandedContainers: ObservableObject { subscript(_ containerID: ContainerIdentifier) -> Bool { get { - if expandedTable.contains(containerID) { - return true - } else { - return false - } + return expandedTable.contains(containerID) } set(newValue) { if newValue { diff --git a/Multiplatform/Shared/Sidebar/SidebarItemView.swift b/Multiplatform/Shared/Sidebar/SidebarItemView.swift index a50b63414..5652fcb16 100644 --- a/Multiplatform/Shared/Sidebar/SidebarItemView.swift +++ b/Multiplatform/Shared/Sidebar/SidebarItemView.swift @@ -11,13 +11,14 @@ import Account struct SidebarItemView: View { - @StateObject var feedImageLoader = FeedImageLoader() + @StateObject var feedIconImageLoader = FeedIconImageLoader() var sidebarItem: SidebarItem var body: some View { HStack { - if let image = feedImageLoader.image { + if let image = feedIconImageLoader.image { IconImageView(iconImage: image) + .frame(width: 20, height: 20, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) } Text(verbatim: sidebarItem.nameForDisplay) Spacer() @@ -27,7 +28,7 @@ struct SidebarItemView: View { } .onAppear { if let feed = sidebarItem.feed { - feedImageLoader.loadImage(for: feed) + feedIconImageLoader.loadImage(for: feed) } }.contextMenu(menuItems: { menuItems diff --git a/Multiplatform/Shared/Sidebar/SidebarModel.swift b/Multiplatform/Shared/Sidebar/SidebarModel.swift index 38aff59a1..335d6b260 100644 --- a/Multiplatform/Shared/Sidebar/SidebarModel.swift +++ b/Multiplatform/Shared/Sidebar/SidebarModel.swift @@ -11,7 +11,6 @@ import RSCore import Account protocol SidebarModelDelegate: class { - func sidebarSelectionDidChange(_: SidebarModel, feeds: [Feed]?) func unreadCount(for: Feed) -> Int } diff --git a/Multiplatform/Shared/Sidebar/SidebarView.swift b/Multiplatform/Shared/Sidebar/SidebarView.swift index 5f85e8242..ba44a48fc 100644 --- a/Multiplatform/Shared/Sidebar/SidebarView.swift +++ b/Multiplatform/Shared/Sidebar/SidebarView.swift @@ -16,8 +16,6 @@ struct SidebarView: View { @StateObject private var expandedContainers = SidebarExpandedContainers() @EnvironmentObject private var sidebarModel: SidebarModel -// @State private var selected = Set() - var body: some View { List() { ForEach(sidebarModel.sidebarItems) { sidebarItem in @@ -27,13 +25,19 @@ struct SidebarView: View { if let containerID = sidebarItem.containerID { DisclosureGroup(isExpanded: $expandedContainers[containerID]) { ForEach(sidebarItem.children) { sidebarItem in - SidebarItemView(sidebarItem: sidebarItem) + NavigationLink(destination: (TimelineContainerView(feed: sidebarItem.feed))) { + SidebarItemView(sidebarItem: sidebarItem) + } } } label: { - SidebarItemView(sidebarItem: sidebarItem) + NavigationLink(destination: (TimelineContainerView(feed: sidebarItem.feed))) { + SidebarItemView(sidebarItem: sidebarItem) + } } } else { - SidebarItemView(sidebarItem: sidebarItem) + NavigationLink(destination: (TimelineContainerView(feed: sidebarItem.feed))) { + SidebarItemView(sidebarItem: sidebarItem) + } } } } label: { diff --git a/Multiplatform/Shared/SwiftUI Extensions/Image-Extensions.swift b/Multiplatform/Shared/SwiftUI Extensions/Image-Extensions.swift new file mode 100644 index 000000000..517991676 --- /dev/null +++ b/Multiplatform/Shared/SwiftUI Extensions/Image-Extensions.swift @@ -0,0 +1,23 @@ +// +// Image-Extensions.swift +// NetNewsWire +// +// Created by Maurice Parker on 7/1/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import SwiftUI +import RSCore + +extension Image { + + init(rsImage: RSImage) { + #if os(macOS) + self = Image(nsImage: rsImage) + #endif + #if os(iOS) + self = Image(uiImage: rsImage) + #endif + } + +} diff --git a/Multiplatform/Shared/Timeline/TimelineContainerView.swift b/Multiplatform/Shared/Timeline/TimelineContainerView.swift index 09e75ad7a..008e28d42 100644 --- a/Multiplatform/Shared/Timeline/TimelineContainerView.swift +++ b/Multiplatform/Shared/Timeline/TimelineContainerView.swift @@ -7,28 +7,26 @@ // import SwiftUI +import Account struct TimelineContainerView: View { @EnvironmentObject private var sceneModel: SceneModel @StateObject private var timelineModel = TimelineModel() + var feed: Feed? = nil @ViewBuilder var body: some View { - TimelineView() - .environmentObject(timelineModel) - .listStyle(SidebarListStyle()) - .onAppear { - sceneModel.timelineModel = timelineModel - timelineModel.delegate = sceneModel - timelineModel.rebuildTimelineItems() - } + if let feed = feed { + TimelineView() + .environmentObject(timelineModel) + .onAppear { + sceneModel.timelineModel = timelineModel + timelineModel.delegate = sceneModel + timelineModel.rebuildTimelineItems(feed) + } + } else { + EmptyView() + } } } - -struct TimelineContainerView_Previews: PreviewProvider { - static var previews: some View { - TimelineContainerView() - .environmentObject(SceneModel()) - } -} diff --git a/Multiplatform/Shared/Timeline/TimelineItem.swift b/Multiplatform/Shared/Timeline/TimelineItem.swift index eb82c20de..faf03baa7 100644 --- a/Multiplatform/Shared/Timeline/TimelineItem.swift +++ b/Multiplatform/Shared/Timeline/TimelineItem.swift @@ -9,9 +9,36 @@ import SwiftUI import Articles +enum TimelineItemStatus { + case showStar + case showUnread + case showNone +} + struct TimelineItem: Identifiable { - var id: String + var article: Article + var id: String { + return article.articleID + } + + var status: TimelineItemStatus { + if article.status.starred == true { + return .showStar + } + if article.status.read == false { + return .showUnread + } + return .showNone + } + + var byline: String { + return article.webFeed?.nameForDisplay ?? "" + } + + var dateTimeString: String { + return ArticleStringFormatter.dateString(article.logicalDatePublished) + } } diff --git a/Multiplatform/Shared/Timeline/TimelineItemStatusView.swift b/Multiplatform/Shared/Timeline/TimelineItemStatusView.swift new file mode 100644 index 000000000..bef9ff233 --- /dev/null +++ b/Multiplatform/Shared/Timeline/TimelineItemStatusView.swift @@ -0,0 +1,43 @@ +// +// TimelineItemStatusView.swift +// NetNewsWire +// +// Created by Maurice Parker on 7/1/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import SwiftUI + +struct TimelineItemStatusView: View { + + var status: TimelineItemStatus + + @ViewBuilder var statusView: some View { + switch status { + case .showUnread: + AppAssets.timelineUnread + .resizable() + .frame(width: 8, height: 8, alignment: .center) + .padding(.all, 2) + .foregroundColor(.accentColor) + case .showStar: + AppAssets.timelineStarred + .resizable() + .frame(width: 10, height: 10, alignment: .center) + .foregroundColor(.yellow) + case .showNone: + AppAssets.timelineUnread + .resizable() + .frame(width: 8, height: 8, alignment: .center) + .padding(.all, 2) + .opacity(0) + } + } + + var body: some View { + statusView + .padding(.top, 4) + .padding(.leading, 4) + } + +} diff --git a/Multiplatform/Shared/Timeline/TimelineItemView.swift b/Multiplatform/Shared/Timeline/TimelineItemView.swift new file mode 100644 index 000000000..fd5529d2b --- /dev/null +++ b/Multiplatform/Shared/Timeline/TimelineItemView.swift @@ -0,0 +1,65 @@ +// +// TimelineItemView.swift +// NetNewsWire +// +// Created by Maurice Parker on 7/1/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import SwiftUI + +struct TimelineItemView: View { + + @StateObject var articleIconImageLoader = ArticleIconImageLoader() + var timelineItem: TimelineItem + + var body: some View { + VStack { + HStack(alignment: .top) { + TimelineItemStatusView(status: timelineItem.status) + if let image = articleIconImageLoader.image { + IconImageView(iconImage: image) + .frame(width: AppDefaults.timelineIconSize.size.width, height: AppDefaults.timelineIconSize.size.height, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) + } + VStack { + Text(verbatim: timelineItem.article.title ?? "N/A") + .fontWeight(.semibold) + .lineLimit(AppDefaults.timelineNumberOfLines) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.trailing, 4) + Spacer() + HStack { + Text(verbatim: timelineItem.byline) + .lineLimit(1) + .truncationMode(.tail) + .font(.footnote) + .foregroundColor(.secondary) + Spacer() + Text(verbatim: timelineItem.dateTimeString) + .lineLimit(1) + .font(.footnote) + .foregroundColor(.secondary) + .padding(.trailing, 4) + } + } + } + Divider() + } + .onAppear { + articleIconImageLoader.loadImage(for: timelineItem.article) + } + } +} + +struct TimelineItemView_Previews: PreviewProvider { + static var previews: some View { + Group { + TimelineItemView(timelineItem: TimelineItem(article: PreviewArticles.basicRead)) + .frame(maxWidth: 250) + TimelineItemView(timelineItem: TimelineItem(article: PreviewArticles.basicUnread)) + .frame(maxWidth: 250) + TimelineItemView(timelineItem: TimelineItem(article: PreviewArticles.basicStarred)) + .frame(maxWidth: 250) + } + } +} diff --git a/Multiplatform/Shared/Timeline/TimelineModel.swift b/Multiplatform/Shared/Timeline/TimelineModel.swift index 0f98ebb4b..6f06fa7ff 100644 --- a/Multiplatform/Shared/Timeline/TimelineModel.swift +++ b/Multiplatform/Shared/Timeline/TimelineModel.swift @@ -9,6 +9,7 @@ import Foundation import RSCore import Account +import Articles protocol TimelineModelDelegate: class { func timelineRequestedWebFeedSelection(_: TimelineModel, webFeed: WebFeed) @@ -20,19 +21,103 @@ class TimelineModel: ObservableObject { @Published var timelineItems = [TimelineItem]() + private var feeds = [Feed]() + private var fetchSerialNumber = 0 + private let fetchRequestQueue = FetchRequestQueue() + private var exceptionArticleFetcher: ArticleFetcher? + private var isReadFiltered = false + + private var articles = [Article]() + + private var sortDirection = AppDefaults.timelineSortDirection { + didSet { + if sortDirection != oldValue { + sortParametersDidChange() + } + } + } + + private var groupByFeed = AppDefaults.timelineGroupByFeed { + didSet { + if groupByFeed != oldValue { + sortParametersDidChange() + } + } + } + init() { } // MARK: API - func rebuildTimelineItems() { - + func rebuildTimelineItems(_ feed: Feed) { + feeds = [feed] + fetchAndReplaceArticlesAsync() } } // MARK: Private + private extension TimelineModel { + func sortParametersDidChange() { + performBlockAndRestoreSelection { + let unsortedArticles = Set(articles) + replaceArticles(with: unsortedArticles) + } + } + func performBlockAndRestoreSelection(_ block: (() -> Void)) { +// let savedSelection = selectedArticleIDs() + block() +// restoreSelection(savedSelection) + } + + // MARK: Article Fetching + + func fetchAndReplaceArticlesAsync() { + cancelPendingAsyncFetches() + + var fetchers = feeds as [ArticleFetcher] + if let fetcher = exceptionArticleFetcher { + fetchers.append(fetcher) + exceptionArticleFetcher = nil + } + + fetchUnsortedArticlesAsync(for: fetchers) { [weak self] (articles) in + self?.replaceArticles(with: articles) + } + } + + func cancelPendingAsyncFetches() { + fetchSerialNumber += 1 + fetchRequestQueue.cancelAllRequests() + } + + func fetchUnsortedArticlesAsync(for representedObjects: [Any], completion: @escaping ArticleSetBlock) { + // The callback will *not* be called if the fetch is no longer relevant — that is, + // if it’s been superseded by a newer fetch, or the timeline was emptied, etc., it won’t get called. + precondition(Thread.isMainThread) + cancelPendingAsyncFetches() + let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: isReadFiltered ?? true, representedObjects: representedObjects) { [weak self] (articles, operation) in + precondition(Thread.isMainThread) + guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else { + return + } + completion(articles) + } + fetchRequestQueue.add(fetchOperation) + } + + func replaceArticles(with unsortedArticles: Set
) { + articles = Array(unsortedArticles).sortedByDate(sortDirection, groupByFeed: groupByFeed) + timelineItems = articles.map { TimelineItem(article: $0) } + + // TODO: Update unread counts and other item done in didSet on AppKit + } + + + // MARK: - Notifications + } diff --git a/Multiplatform/Shared/Timeline/TimelineView.swift b/Multiplatform/Shared/Timeline/TimelineView.swift index 486d4f6c5..1989e5ea5 100644 --- a/Multiplatform/Shared/Timeline/TimelineView.swift +++ b/Multiplatform/Shared/Timeline/TimelineView.swift @@ -9,13 +9,23 @@ import SwiftUI struct TimelineView: View { - var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - } -} + + @EnvironmentObject private var timelineModel: TimelineModel -struct TimelineView_Previews: PreviewProvider { - static var previews: some View { - TimelineView() + var body: some View { + ScrollView { + LazyVStack() { + ForEach(timelineModel.timelineItems) { timelineItem in + TimelineItemView(timelineItem: timelineItem) + } + } + } } + +// var body: some View { +// List(timelineModel.timelineItems) { timelineItem in +// TimelineItemView(timelineItem: timelineItem) +// } +// } + } diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index fbd8a090d..32ea67edc 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -149,6 +149,16 @@ 514A89A6244FD6640085E65D /* AddTwitterFeedWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514A89A4244FD6640085E65D /* AddTwitterFeedWindowController.swift */; }; 514B7C8323205EFB00BAC947 /* RootSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514B7C8223205EFB00BAC947 /* RootSplitViewController.swift */; }; 514B7D1F23219F3C00BAC947 /* AddControllerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514B7D1E23219F3C00BAC947 /* AddControllerType.swift */; }; + 514E6BDA24ACEA0400AC6F6E /* TimelineItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6BD924ACEA0400AC6F6E /* TimelineItemView.swift */; }; + 514E6BDB24ACEA0400AC6F6E /* TimelineItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6BD924ACEA0400AC6F6E /* TimelineItemView.swift */; }; + 514E6BFF24AD255D00AC6F6E /* PreviewArticles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6BFE24AD255D00AC6F6E /* PreviewArticles.swift */; }; + 514E6C0024AD255D00AC6F6E /* PreviewArticles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6BFE24AD255D00AC6F6E /* PreviewArticles.swift */; }; + 514E6C0224AD29A300AC6F6E /* TimelineItemStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6C0124AD29A300AC6F6E /* TimelineItemStatusView.swift */; }; + 514E6C0324AD29A300AC6F6E /* TimelineItemStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6C0124AD29A300AC6F6E /* TimelineItemStatusView.swift */; }; + 514E6C0624AD2B5F00AC6F6E /* Image-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6C0524AD2B5F00AC6F6E /* Image-Extensions.swift */; }; + 514E6C0724AD2B5F00AC6F6E /* Image-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6C0524AD2B5F00AC6F6E /* Image-Extensions.swift */; }; + 514E6C0924AD39AD00AC6F6E /* ArticleIconImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6C0824AD39AD00AC6F6E /* ArticleIconImageLoader.swift */; }; + 514E6C0A24AD39AD00AC6F6E /* ArticleIconImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6C0824AD39AD00AC6F6E /* ArticleIconImageLoader.swift */; }; 5154368B229404D1005E1CDF /* FaviconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F76227716200050506E /* FaviconGenerator.swift */; }; 51554C24228B71910055115A /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; }; 51554C25228B71910055115A /* SyncDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -222,8 +232,8 @@ 51919FAD24AA8CCA00541E64 /* UnreadCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FAB24AA8CCA00541E64 /* UnreadCountView.swift */; }; 51919FAF24AA8EFA00541E64 /* SidebarItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FAE24AA8EFA00541E64 /* SidebarItemView.swift */; }; 51919FB024AA8EFA00541E64 /* SidebarItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FAE24AA8EFA00541E64 /* SidebarItemView.swift */; }; - 51919FB324AAB97900541E64 /* FeedImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FB224AAB97900541E64 /* FeedImageLoader.swift */; }; - 51919FB424AAB97900541E64 /* FeedImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FB224AAB97900541E64 /* FeedImageLoader.swift */; }; + 51919FB324AAB97900541E64 /* FeedIconImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FB224AAB97900541E64 /* FeedIconImageLoader.swift */; }; + 51919FB424AAB97900541E64 /* FeedIconImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FB224AAB97900541E64 /* FeedIconImageLoader.swift */; }; 51919FB624AABCA100541E64 /* IconImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FB524AABCA100541E64 /* IconImageView.swift */; }; 51919FB724AABCA100541E64 /* IconImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FB524AABCA100541E64 /* IconImageView.swift */; }; 51919FEE24AB85E400541E64 /* TimelineContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FED24AB85E400541E64 /* TimelineContainerView.swift */; }; @@ -1781,6 +1791,11 @@ 514A89A4244FD6640085E65D /* AddTwitterFeedWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AddTwitterFeedWindowController.swift; path = AddFeed/AddTwitterFeedWindowController.swift; sourceTree = ""; }; 514B7C8223205EFB00BAC947 /* RootSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootSplitViewController.swift; sourceTree = ""; }; 514B7D1E23219F3C00BAC947 /* AddControllerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddControllerType.swift; sourceTree = ""; }; + 514E6BD924ACEA0400AC6F6E /* TimelineItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemView.swift; sourceTree = ""; }; + 514E6BFE24AD255D00AC6F6E /* PreviewArticles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewArticles.swift; sourceTree = ""; }; + 514E6C0124AD29A300AC6F6E /* TimelineItemStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemStatusView.swift; sourceTree = ""; }; + 514E6C0524AD2B5F00AC6F6E /* Image-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Image-Extensions.swift"; sourceTree = ""; }; + 514E6C0824AD39AD00AC6F6E /* ArticleIconImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleIconImageLoader.swift; sourceTree = ""; }; 51554BFC228B6EB50055115A /* SyncDatabase.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SyncDatabase.xcodeproj; path = Frameworks/SyncDatabase/SyncDatabase.xcodeproj; sourceTree = SOURCE_ROOT; }; 515A50E5243D07A90089E588 /* ExtensionPointManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionPointManager.swift; sourceTree = ""; }; 515A5106243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TwitterFeedProvider-Extensions.swift"; sourceTree = ""; }; @@ -1827,7 +1842,7 @@ 51919FA524AA64B000541E64 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = ""; }; 51919FAB24AA8CCA00541E64 /* UnreadCountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnreadCountView.swift; sourceTree = ""; }; 51919FAE24AA8EFA00541E64 /* SidebarItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarItemView.swift; sourceTree = ""; }; - 51919FB224AAB97900541E64 /* FeedImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedImageLoader.swift; sourceTree = ""; }; + 51919FB224AAB97900541E64 /* FeedIconImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedIconImageLoader.swift; sourceTree = ""; }; 51919FB524AABCA100541E64 /* IconImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImageView.swift; sourceTree = ""; }; 51919FED24AB85E400541E64 /* TimelineContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineContainerView.swift; sourceTree = ""; }; 51919FF024AB864A00541E64 /* TimelineModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineModel.swift; sourceTree = ""; }; @@ -2521,6 +2536,22 @@ path = OPML; sourceTree = ""; }; + 514E6BFD24AD252400AC6F6E /* Previews */ = { + isa = PBXGroup; + children = ( + 514E6BFE24AD255D00AC6F6E /* PreviewArticles.swift */, + ); + path = Previews; + sourceTree = ""; + }; + 514E6C0424AD2B0400AC6F6E /* SwiftUI Extensions */ = { + isa = PBXGroup; + children = ( + 514E6C0524AD2B5F00AC6F6E /* Image-Extensions.swift */, + ); + path = "SwiftUI Extensions"; + sourceTree = ""; + }; 51554BFD228B6EB50055115A /* Products */ = { isa = PBXGroup; children = ( @@ -2611,7 +2642,8 @@ 51919FB124AAB95300541E64 /* Images */ = { isa = PBXGroup; children = ( - 51919FB224AAB97900541E64 /* FeedImageLoader.swift */, + 514E6C0824AD39AD00AC6F6E /* ArticleIconImageLoader.swift */, + 51919FB224AAB97900541E64 /* FeedIconImageLoader.swift */, 51919FB524AABCA100541E64 /* IconImageView.swift */, ); path = Images; @@ -2622,8 +2654,10 @@ children = ( 51919FED24AB85E400541E64 /* TimelineContainerView.swift */, 51919FF324AB869C00541E64 /* TimelineItem.swift */, + 514E6BD924ACEA0400AC6F6E /* TimelineItemView.swift */, 51919FF024AB864A00541E64 /* TimelineModel.swift */, 51919FF624AB8B7700541E64 /* TimelineView.swift */, + 514E6C0124AD29A300AC6F6E /* TimelineItemStatusView.swift */, ); path = Timeline; sourceTree = ""; @@ -2696,7 +2730,9 @@ 51E49A0224A91FF600B667CB /* SceneNavigationView.swift */, 51C0513824A77DF800194D5E /* Assets.xcassets */, 51919FB124AAB95300541E64 /* Images */, + 514E6BFD24AD252400AC6F6E /* Previews */, 51E499FB24A9135A00B667CB /* Sidebar */, + 514E6C0424AD2B0400AC6F6E /* SwiftUI Extensions */, 51919FCB24AB855000541E64 /* Timeline */, ); path = Shared; @@ -4723,9 +4759,12 @@ 51E498F124A8085D00B667CB /* StarredFeedDelegate.swift in Sources */, 51E498FF24A808BB00B667CB /* SingleFaviconDownloader.swift in Sources */, 51E4997224A8784300B667CB /* DefaultFeedsImporter.swift in Sources */, + 514E6C0924AD39AD00AC6F6E /* ArticleIconImageLoader.swift in Sources */, 51919FAF24AA8EFA00541E64 /* SidebarItemView.swift in Sources */, + 514E6BDA24ACEA0400AC6F6E /* TimelineItemView.swift in Sources */, 51E4990D24A808C500B667CB /* RSHTMLMetadata+Extension.swift in Sources */, 51919FF424AB869C00541E64 /* TimelineItem.swift in Sources */, + 514E6C0224AD29A300AC6F6E /* TimelineItemStatusView.swift in Sources */, 51E49A0024A91FC100B667CB /* RegularSidebarContainerView.swift in Sources */, 51E4995C24A875F300B667CB /* ArticleRenderer.swift in Sources */, 51E4992324A8095700B667CB /* URL-Extensions.swift in Sources */, @@ -4737,6 +4776,7 @@ 172199F124AB716900A31D04 /* SidebarToolbar.swift in Sources */, 51E4990B24A808C500B667CB /* ImageDownloader.swift in Sources */, 51E498F424A8085D00B667CB /* SmartFeedDelegate.swift in Sources */, + 514E6BFF24AD255D00AC6F6E /* PreviewArticles.swift in Sources */, 51E4993024A8676400B667CB /* ArticleSorter.swift in Sources */, 51408B7E24A9EC6F0073CF4E /* SidebarItem.swift in Sources */, 51E4990A24A808C500B667CB /* FeaturedImageDownloader.swift in Sources */, @@ -4763,6 +4803,7 @@ 51E4990F24A808CC00B667CB /* HTMLMetadataDownloader.swift in Sources */, 51E4993124A8676400B667CB /* FetchRequestOperation.swift in Sources */, 51E4992624A80AAB00B667CB /* AppAssets.swift in Sources */, + 514E6C0624AD2B5F00AC6F6E /* Image-Extensions.swift in Sources */, 51E4995624A8734D00B667CB /* TwitterFeedProvider-Extensions.swift in Sources */, 51E4996824A8760C00B667CB /* ArticleStyle.swift in Sources */, 51E4990024A808BB00B667CB /* FaviconGenerator.swift in Sources */, @@ -4770,7 +4811,7 @@ 51E4991E24A8094300B667CB /* RSImage-AppIcons.swift in Sources */, 51E499D824A912C200B667CB /* SceneModel.swift in Sources */, 51919FB324AAB97900541E64 /* FeedImageLoader.swift in Sources */, - 17B223DC24AC24D2001E4592 /* TimelineLayoutView.swift in Sources */, + 51919FB324AAB97900541E64 /* FeedIconImageLoader.swift in Sources */, 51E4991324A808FB00B667CB /* AddWebFeedDefaultContainer.swift in Sources */, 51E4993C24A8709900B667CB /* AppDelegate.swift in Sources */, 51E498F924A8085D00B667CB /* SmartFeed.swift in Sources */, @@ -4817,6 +4858,7 @@ 51E4990824A808C300B667CB /* RSHTMLMetadata+Extension.swift in Sources */, 51919FF824AB8B7700541E64 /* TimelineView.swift in Sources */, 51E4992B24A8676300B667CB /* ArticleArray.swift in Sources */, + 514E6C0724AD2B5F00AC6F6E /* Image-Extensions.swift in Sources */, 51E4994D24A8734C00B667CB /* ExtensionPointIdentifer.swift in Sources */, 51E4992224A8095600B667CB /* URL-Extensions.swift in Sources */, 51E4990424A808C300B667CB /* WebFeedIconDownloader.swift in Sources */, @@ -4842,8 +4884,9 @@ 51E498FC24A808BA00B667CB /* FaviconURLFinder.swift in Sources */, 51E4991C24A8092000B667CB /* NSAttributedString+NetNewsWire.swift in Sources */, 51E499D924A912C200B667CB /* SceneModel.swift in Sources */, - 51919FB424AAB97900541E64 /* FeedImageLoader.swift in Sources */, + 51919FB424AAB97900541E64 /* FeedIconImageLoader.swift in Sources */, 51E4994A24A8734C00B667CB /* ExtensionPointManager.swift in Sources */, + 514E6C0324AD29A300AC6F6E /* TimelineItemStatusView.swift in Sources */, 51E4996D24A8762D00B667CB /* ArticleExtractor.swift in Sources */, 51E4994024A8713B00B667CB /* AccountRefreshTimer.swift in Sources */, 51E49A0424A91FF600B667CB /* SceneNavigationView.swift in Sources */, @@ -4851,12 +4894,14 @@ 51E498C824A8085D00B667CB /* SmartFeedsController.swift in Sources */, 175942AB24AD533200585066 /* RefreshInterval.swift in Sources */, 51E4992C24A8676300B667CB /* ArticleSorter.swift in Sources */, + 514E6C0A24AD39AD00AC6F6E /* ArticleIconImageLoader.swift in Sources */, 51E4995024A8734C00B667CB /* ExtensionPoint.swift in Sources */, 51E4990E24A808CC00B667CB /* HTMLMetadataDownloader.swift in Sources */, 51E498FB24A808BA00B667CB /* FaviconGenerator.swift in Sources */, 51E4996724A8760B00B667CB /* ArticleStylesManager.swift in Sources */, 1729529B24AA1FD200D65E66 /* MacSearchField.swift in Sources */, 51408B7F24A9EC6F0073CF4E /* SidebarItem.swift in Sources */, + 514E6BDB24ACEA0400AC6F6E /* TimelineItemView.swift in Sources */, 51E4996E24A8764C00B667CB /* ActivityManager.swift in Sources */, 51E4995A24A873F900B667CB /* ErrorHandler.swift in Sources */, 51E4991F24A8094300B667CB /* RSImage-AppIcons.swift in Sources */, @@ -4877,6 +4922,7 @@ 51E4996124A875F400B667CB /* ArticleRenderer.swift in Sources */, 51392D1C24AC19A000BE0D35 /* SidebarExpandedContainers.swift in Sources */, 51C0515F24A77DF800194D5E /* MainApp.swift in Sources */, + 514E6C0024AD255D00AC6F6E /* PreviewArticles.swift in Sources */, 1729529524AA1CAA00D65E66 /* GeneralPreferencesView.swift in Sources */, 1729529424AA1CAA00D65E66 /* AdvancedPreferencesView.swift in Sources */, 51E4992D24A8676300B667CB /* FetchRequestOperation.swift in Sources */,