From e7a04804b4ae7c86972afc7f015b5ea5ff850477 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 1 Jul 2020 03:46:00 -0500 Subject: [PATCH 01/10] Simplified the if/else logic in the getter --- .../Shared/Sidebar/SidebarExpandedContainers.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 { From c1eb9ab927d80d038e76c6e3640f029b234cda4d Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 1 Jul 2020 11:13:11 -0500 Subject: [PATCH 02/10] Add sidebar navigation --- .../Shared/Sidebar/SidebarView.swift | 14 +++++++---- .../Timeline/TimelineContainerView.swift | 21 +++++++++------- .../Shared/Timeline/TimelineItem.swift | 5 +++- .../Shared/Timeline/TimelineItemView.swift | 24 +++++++++++++++++++ .../Shared/Timeline/TimelineModel.swift | 6 +++-- .../Shared/Timeline/TimelineView.swift | 5 +++- NetNewsWire.xcodeproj/project.pbxproj | 6 +++++ 7 files changed, 64 insertions(+), 17 deletions(-) create mode 100644 Multiplatform/Shared/Timeline/TimelineItemView.swift 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/Timeline/TimelineContainerView.swift b/Multiplatform/Shared/Timeline/TimelineContainerView.swift index 09e75ad7a..010731817 100644 --- a/Multiplatform/Shared/Timeline/TimelineContainerView.swift +++ b/Multiplatform/Shared/Timeline/TimelineContainerView.swift @@ -7,21 +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() + } } } diff --git a/Multiplatform/Shared/Timeline/TimelineItem.swift b/Multiplatform/Shared/Timeline/TimelineItem.swift index eb82c20de..c15c6c432 100644 --- a/Multiplatform/Shared/Timeline/TimelineItem.swift +++ b/Multiplatform/Shared/Timeline/TimelineItem.swift @@ -11,7 +11,10 @@ import Articles struct TimelineItem: Identifiable { - var id: String + var article: Article + var id: String { + return article.articleID + } } diff --git a/Multiplatform/Shared/Timeline/TimelineItemView.swift b/Multiplatform/Shared/Timeline/TimelineItemView.swift new file mode 100644 index 000000000..d98943a2a --- /dev/null +++ b/Multiplatform/Shared/Timeline/TimelineItemView.swift @@ -0,0 +1,24 @@ +// +// TimelineItemView.swift +// NetNewsWire +// +// Created by Maurice Parker on 7/1/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import SwiftUI + +struct TimelineItemView: View { + + var timelineItem: TimelineItem + + var body: some View { + Text(verbatim: timelineItem.article.title ?? "N/A") + } +} + +//struct TimelineItemView_Previews: PreviewProvider { +// static var previews: some View { +// TimelineItemView() +// } +//} diff --git a/Multiplatform/Shared/Timeline/TimelineModel.swift b/Multiplatform/Shared/Timeline/TimelineModel.swift index 0f98ebb4b..b21da157e 100644 --- a/Multiplatform/Shared/Timeline/TimelineModel.swift +++ b/Multiplatform/Shared/Timeline/TimelineModel.swift @@ -20,13 +20,15 @@ class TimelineModel: ObservableObject { @Published var timelineItems = [TimelineItem]() + private var feeds = [Feed]() + init() { } // MARK: API - func rebuildTimelineItems() { - + func rebuildTimelineItems(_ feed: Feed) { + feeds = [feed] } } diff --git a/Multiplatform/Shared/Timeline/TimelineView.swift b/Multiplatform/Shared/Timeline/TimelineView.swift index 486d4f6c5..321a5cd2e 100644 --- a/Multiplatform/Shared/Timeline/TimelineView.swift +++ b/Multiplatform/Shared/Timeline/TimelineView.swift @@ -9,7 +9,10 @@ import SwiftUI struct TimelineView: View { - var body: some View { + + @EnvironmentObject private var timelineModel: TimelineModel + + var body: some View { Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) } } diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index b71450964..4173f467b 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -145,6 +145,8 @@ 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 */; }; 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, ); }; }; @@ -1780,6 +1782,7 @@ 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 = ""; }; 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 = ""; }; @@ -2622,6 +2625,7 @@ children = ( 51919FED24AB85E400541E64 /* TimelineContainerView.swift */, 51919FF324AB869C00541E64 /* TimelineItem.swift */, + 514E6BD924ACEA0400AC6F6E /* TimelineItemView.swift */, 51919FF024AB864A00541E64 /* TimelineModel.swift */, 51919FF624AB8B7700541E64 /* TimelineView.swift */, ); @@ -4725,6 +4729,7 @@ 51E498FF24A808BB00B667CB /* SingleFaviconDownloader.swift in Sources */, 51E4997224A8784300B667CB /* DefaultFeedsImporter.swift in Sources */, 51919FAF24AA8EFA00541E64 /* SidebarItemView.swift in Sources */, + 514E6BDA24ACEA0400AC6F6E /* TimelineItemView.swift in Sources */, 51E4990D24A808C500B667CB /* RSHTMLMetadata+Extension.swift in Sources */, 51919FF424AB869C00541E64 /* TimelineItem.swift in Sources */, 51E49A0024A91FC100B667CB /* RegularSidebarContainerView.swift in Sources */, @@ -4855,6 +4860,7 @@ 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 */, From 74f34b1c7865aeb0b8616882b26af781378103e9 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 1 Jul 2020 12:30:55 -0500 Subject: [PATCH 03/10] Add basic timeline fetching --- Multiplatform/Shared/SceneModel.swift | 4 - .../Shared/Sidebar/SidebarModel.swift | 1 - .../Timeline/TimelineContainerView.swift | 7 -- .../Shared/Timeline/TimelineItemView.swift | 6 +- .../Shared/Timeline/TimelineModel.swift | 83 +++++++++++++++++++ .../Shared/Timeline/TimelineView.swift | 21 +++-- 6 files changed, 102 insertions(+), 20 deletions(-) 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/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/Timeline/TimelineContainerView.swift b/Multiplatform/Shared/Timeline/TimelineContainerView.swift index 010731817..008e28d42 100644 --- a/Multiplatform/Shared/Timeline/TimelineContainerView.swift +++ b/Multiplatform/Shared/Timeline/TimelineContainerView.swift @@ -30,10 +30,3 @@ struct TimelineContainerView: View { } } - -struct TimelineContainerView_Previews: PreviewProvider { - static var previews: some View { - TimelineContainerView() - .environmentObject(SceneModel()) - } -} diff --git a/Multiplatform/Shared/Timeline/TimelineItemView.swift b/Multiplatform/Shared/Timeline/TimelineItemView.swift index d98943a2a..021c7cb5c 100644 --- a/Multiplatform/Shared/Timeline/TimelineItemView.swift +++ b/Multiplatform/Shared/Timeline/TimelineItemView.swift @@ -13,7 +13,11 @@ struct TimelineItemView: View { var timelineItem: TimelineItem var body: some View { - Text(verbatim: timelineItem.article.title ?? "N/A") + VStack { + Text(verbatim: timelineItem.article.title ?? "N/A") + .frame(maxWidth: .infinity, alignment: .leading) + Divider() + } } } diff --git a/Multiplatform/Shared/Timeline/TimelineModel.swift b/Multiplatform/Shared/Timeline/TimelineModel.swift index b21da157e..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) @@ -21,6 +22,28 @@ 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() { } @@ -29,12 +52,72 @@ class TimelineModel: ObservableObject { 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 321a5cd2e..1989e5ea5 100644 --- a/Multiplatform/Shared/Timeline/TimelineView.swift +++ b/Multiplatform/Shared/Timeline/TimelineView.swift @@ -13,12 +13,19 @@ struct TimelineView: View { @EnvironmentObject private var timelineModel: TimelineModel var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - } -} - -struct TimelineView_Previews: PreviewProvider { - static var previews: some View { - TimelineView() + ScrollView { + LazyVStack() { + ForEach(timelineModel.timelineItems) { timelineItem in + TimelineItemView(timelineItem: timelineItem) + } + } + } } + +// var body: some View { +// List(timelineModel.timelineItems) { timelineItem in +// TimelineItemView(timelineItem: timelineItem) +// } +// } + } From 781f24454e0196af824c2a3538dd6deb8b33d3e2 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 1 Jul 2020 16:33:07 -0500 Subject: [PATCH 04/10] Add read/starred indicator to timeline --- Multiplatform/Shared/AppAssets.swift | 10 ++++ .../Shared/Images/IconImageView.swift | 11 +--- .../Shared/Previews/PreviewArticles.swift | 58 +++++++++++++++++++ .../SwiftUI Extensions/Image-Extensions.swift | 23 ++++++++ .../Shared/Timeline/TimelineItem.swift | 16 +++++ .../Timeline/TimelineItemStatusView.swift | 40 +++++++++++++ .../Shared/Timeline/TimelineItemView.swift | 24 +++++--- NetNewsWire.xcodeproj/project.pbxproj | 34 +++++++++++ 8 files changed, 199 insertions(+), 17 deletions(-) create mode 100644 Multiplatform/Shared/Previews/PreviewArticles.swift create mode 100644 Multiplatform/Shared/SwiftUI Extensions/Image-Extensions.swift create mode 100644 Multiplatform/Shared/Timeline/TimelineItemStatusView.swift 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/IconImageView.swift b/Multiplatform/Shared/Images/IconImageView.swift index 3eb74a1ca..83bdcf7be 100644 --- a/Multiplatform/Shared/Images/IconImageView.swift +++ b/Multiplatform/Shared/Images/IconImageView.swift @@ -13,20 +13,11 @@ 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/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/TimelineItem.swift b/Multiplatform/Shared/Timeline/TimelineItem.swift index c15c6c432..730530b1a 100644 --- a/Multiplatform/Shared/Timeline/TimelineItem.swift +++ b/Multiplatform/Shared/Timeline/TimelineItem.swift @@ -9,6 +9,12 @@ import SwiftUI import Articles +enum TimelineItemStatus { + case showStar + case showUnread + case showNone +} + struct TimelineItem: Identifiable { var article: Article @@ -17,4 +23,14 @@ struct TimelineItem: Identifiable { return article.articleID } + var status: TimelineItemStatus { + if article.status.starred == true { + return .showStar + } + if article.status.read == false { + return .showUnread + } + return .showNone + } + } diff --git a/Multiplatform/Shared/Timeline/TimelineItemStatusView.swift b/Multiplatform/Shared/Timeline/TimelineItemStatusView.swift new file mode 100644 index 000000000..766aba6dd --- /dev/null +++ b/Multiplatform/Shared/Timeline/TimelineItemStatusView.swift @@ -0,0 +1,40 @@ +// +// 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: + Spacer() + .frame(width: 10, height: 10, alignment: .center) + } + } + + 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 index 021c7cb5c..b5c1b2138 100644 --- a/Multiplatform/Shared/Timeline/TimelineItemView.swift +++ b/Multiplatform/Shared/Timeline/TimelineItemView.swift @@ -14,15 +14,25 @@ struct TimelineItemView: View { var body: some View { VStack { - Text(verbatim: timelineItem.article.title ?? "N/A") - .frame(maxWidth: .infinity, alignment: .leading) + HStack(alignment: .top) { + TimelineItemStatusView(status: timelineItem.status) + Text(verbatim: timelineItem.article.title ?? "N/A") + .frame(maxWidth: .infinity, alignment: .leading) + } Divider() } } } -//struct TimelineItemView_Previews: PreviewProvider { -// static var previews: some View { -// TimelineItemView() -// } -//} +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/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 4173f467b..425d3cf58 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -147,6 +147,12 @@ 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 */; }; 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, ); }; }; @@ -1783,6 +1789,9 @@ 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 = ""; }; 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 = ""; }; @@ -2524,6 +2533,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 = ( @@ -2628,6 +2653,7 @@ 514E6BD924ACEA0400AC6F6E /* TimelineItemView.swift */, 51919FF024AB864A00541E64 /* TimelineModel.swift */, 51919FF624AB8B7700541E64 /* TimelineView.swift */, + 514E6C0124AD29A300AC6F6E /* TimelineItemStatusView.swift */, ); path = Timeline; sourceTree = ""; @@ -2700,7 +2726,9 @@ 51E49A0224A91FF600B667CB /* SceneNavigationView.swift */, 51C0513824A77DF800194D5E /* Assets.xcassets */, 51919FB124AAB95300541E64 /* Images */, + 514E6BFD24AD252400AC6F6E /* Previews */, 51E499FB24A9135A00B667CB /* Sidebar */, + 514E6C0424AD2B0400AC6F6E /* SwiftUI Extensions */, 51919FCB24AB855000541E64 /* Timeline */, ); path = Shared; @@ -4732,6 +4760,7 @@ 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 */, @@ -4743,6 +4772,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 */, @@ -4769,6 +4799,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 */, 51E4992924A866F000B667CB /* AppDefaults.swift in Sources */, 51E4996824A8760C00B667CB /* ArticleStyle.swift in Sources */, @@ -4821,6 +4852,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 */, @@ -4848,6 +4880,7 @@ 51E499D924A912C200B667CB /* SceneModel.swift in Sources */, 51919FB424AAB97900541E64 /* FeedImageLoader.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 */, @@ -4883,6 +4916,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 */, From 4c148e6eba1709334cb1a6267bb31cd690196ae6 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 1 Jul 2020 16:37:20 -0500 Subject: [PATCH 05/10] Rename FeedImageLoader --- ...edImageLoader.swift => FeedIconImageLoader.swift} | 6 +++--- Multiplatform/Shared/Sidebar/SidebarItemView.swift | 6 +++--- NetNewsWire.xcodeproj/project.pbxproj | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) rename Multiplatform/Shared/Images/{FeedImageLoader.swift => FeedIconImageLoader.swift} (91%) diff --git a/Multiplatform/Shared/Images/FeedImageLoader.swift b/Multiplatform/Shared/Images/FeedIconImageLoader.swift similarity index 91% rename from Multiplatform/Shared/Images/FeedImageLoader.swift rename to Multiplatform/Shared/Images/FeedIconImageLoader.swift index 5dc6a40c7..4893af107 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? @@ -41,7 +41,7 @@ final class FeedImageLoader: ObservableObject { } -private extension FeedImageLoader { +private extension FeedIconImageLoader { @objc func faviconDidBecomeAvailable(_ note: Notification) { guard let feed = feed else { return } diff --git a/Multiplatform/Shared/Sidebar/SidebarItemView.swift b/Multiplatform/Shared/Sidebar/SidebarItemView.swift index a50b63414..b48d169fe 100644 --- a/Multiplatform/Shared/Sidebar/SidebarItemView.swift +++ b/Multiplatform/Shared/Sidebar/SidebarItemView.swift @@ -11,12 +11,12 @@ 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) } Text(verbatim: sidebarItem.nameForDisplay) @@ -27,7 +27,7 @@ struct SidebarItemView: View { } .onAppear { if let feed = sidebarItem.feed { - feedImageLoader.loadImage(for: feed) + feedIconImageLoader.loadImage(for: feed) } }.contextMenu(menuItems: { menuItems diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 425d3cf58..9fba717ec 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -226,8 +226,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 */; }; @@ -1838,7 +1838,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 = ""; }; @@ -2639,7 +2639,7 @@ 51919FB124AAB95300541E64 /* Images */ = { isa = PBXGroup; children = ( - 51919FB224AAB97900541E64 /* FeedImageLoader.swift */, + 51919FB224AAB97900541E64 /* FeedIconImageLoader.swift */, 51919FB524AABCA100541E64 /* IconImageView.swift */, ); path = Images; @@ -4807,7 +4807,7 @@ 51E4997124A8764C00B667CB /* ActivityType.swift in Sources */, 51E4991E24A8094300B667CB /* RSImage-AppIcons.swift in Sources */, 51E499D824A912C200B667CB /* SceneModel.swift in Sources */, - 51919FB324AAB97900541E64 /* FeedImageLoader.swift in Sources */, + 51919FB324AAB97900541E64 /* FeedIconImageLoader.swift in Sources */, 51E4991324A808FB00B667CB /* AddWebFeedDefaultContainer.swift in Sources */, 51E4993C24A8709900B667CB /* AppDelegate.swift in Sources */, 51E498F924A8085D00B667CB /* SmartFeed.swift in Sources */, @@ -4878,7 +4878,7 @@ 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 */, From bacffbadbe497455642090492a54497dba8d42d7 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 1 Jul 2020 17:21:58 -0500 Subject: [PATCH 06/10] Add icon image to Timeline --- .../Images/ArticleIconImageLoader.swift | 58 +++++++++++++++++++ .../Shared/Images/IconImageView.swift | 1 - .../Shared/Sidebar/SidebarItemView.swift | 1 + .../Timeline/TimelineItemStatusView.swift | 7 ++- .../Shared/Timeline/TimelineItemView.swift | 8 +++ NetNewsWire.xcodeproj/project.pbxproj | 6 ++ 6 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 Multiplatform/Shared/Images/ArticleIconImageLoader.swift diff --git a/Multiplatform/Shared/Images/ArticleIconImageLoader.swift b/Multiplatform/Shared/Images/ArticleIconImageLoader.swift new file mode 100644 index 000000000..bae640cc8 --- /dev/null +++ b/Multiplatform/Shared/Images/ArticleIconImageLoader.swift @@ -0,0 +1,58 @@ +// +// 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) { + 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 { + + return + } + } + } +} diff --git a/Multiplatform/Shared/Images/IconImageView.swift b/Multiplatform/Shared/Images/IconImageView.swift index 83bdcf7be..95a187518 100644 --- a/Multiplatform/Shared/Images/IconImageView.swift +++ b/Multiplatform/Shared/Images/IconImageView.swift @@ -16,7 +16,6 @@ struct IconImageView: View { return Image(rsImage: iconImage.image) .resizable() .scaledToFit() - .frame(width: 20, height: 20, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) .cornerRadius(4) } } diff --git a/Multiplatform/Shared/Sidebar/SidebarItemView.swift b/Multiplatform/Shared/Sidebar/SidebarItemView.swift index b48d169fe..5652fcb16 100644 --- a/Multiplatform/Shared/Sidebar/SidebarItemView.swift +++ b/Multiplatform/Shared/Sidebar/SidebarItemView.swift @@ -18,6 +18,7 @@ struct SidebarItemView: View { HStack { 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() diff --git a/Multiplatform/Shared/Timeline/TimelineItemStatusView.swift b/Multiplatform/Shared/Timeline/TimelineItemStatusView.swift index 766aba6dd..bef9ff233 100644 --- a/Multiplatform/Shared/Timeline/TimelineItemStatusView.swift +++ b/Multiplatform/Shared/Timeline/TimelineItemStatusView.swift @@ -26,8 +26,11 @@ struct TimelineItemStatusView: View { .frame(width: 10, height: 10, alignment: .center) .foregroundColor(.yellow) case .showNone: - Spacer() - .frame(width: 10, height: 10, alignment: .center) + AppAssets.timelineUnread + .resizable() + .frame(width: 8, height: 8, alignment: .center) + .padding(.all, 2) + .opacity(0) } } diff --git a/Multiplatform/Shared/Timeline/TimelineItemView.swift b/Multiplatform/Shared/Timeline/TimelineItemView.swift index b5c1b2138..4f6fbe96a 100644 --- a/Multiplatform/Shared/Timeline/TimelineItemView.swift +++ b/Multiplatform/Shared/Timeline/TimelineItemView.swift @@ -10,17 +10,25 @@ 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@*/) + } Text(verbatim: timelineItem.article.title ?? "N/A") .frame(maxWidth: .infinity, alignment: .leading) } Divider() } + .onAppear { + articleIconImageLoader.loadImage(for: timelineItem.article) + } } } diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 9fba717ec..0fd783364 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -153,6 +153,8 @@ 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, ); }; }; @@ -1792,6 +1794,7 @@ 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 = ""; }; @@ -2639,6 +2642,7 @@ 51919FB124AAB95300541E64 /* Images */ = { isa = PBXGroup; children = ( + 514E6C0824AD39AD00AC6F6E /* ArticleIconImageLoader.swift */, 51919FB224AAB97900541E64 /* FeedIconImageLoader.swift */, 51919FB524AABCA100541E64 /* IconImageView.swift */, ); @@ -4756,6 +4760,7 @@ 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 */, @@ -4887,6 +4892,7 @@ 51E498CC24A8085D00B667CB /* SearchFeedDelegate.swift in Sources */, 51E498C824A8085D00B667CB /* SmartFeedsController.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 */, From 0b36498ff83f8d279ec0314cfc5591e91d49c614 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 1 Jul 2020 17:39:27 -0500 Subject: [PATCH 07/10] Added date and byline to timeline --- .../Shared/Timeline/TimelineItem.swift | 8 ++++++++ .../Shared/Timeline/TimelineItemView.swift | 19 +++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Multiplatform/Shared/Timeline/TimelineItem.swift b/Multiplatform/Shared/Timeline/TimelineItem.swift index 730530b1a..786854f4b 100644 --- a/Multiplatform/Shared/Timeline/TimelineItem.swift +++ b/Multiplatform/Shared/Timeline/TimelineItem.swift @@ -33,4 +33,12 @@ struct TimelineItem: Identifiable { return .showNone } + var byline: String { + return article.byline() + } + + var dateTimeString: String { + return ArticleStringFormatter.dateString(article.logicalDatePublished) + } + } diff --git a/Multiplatform/Shared/Timeline/TimelineItemView.swift b/Multiplatform/Shared/Timeline/TimelineItemView.swift index 4f6fbe96a..dd952b9c7 100644 --- a/Multiplatform/Shared/Timeline/TimelineItemView.swift +++ b/Multiplatform/Shared/Timeline/TimelineItemView.swift @@ -21,8 +21,23 @@ struct TimelineItemView: View { IconImageView(iconImage: image) .frame(width: AppDefaults.timelineIconSize.size.width, height: AppDefaults.timelineIconSize.size.height, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) } - Text(verbatim: timelineItem.article.title ?? "N/A") - .frame(maxWidth: .infinity, alignment: .leading) + VStack { + Text(verbatim: timelineItem.article.title ?? "N/A") + .lineLimit(3) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.trailing, 4) + Spacer() + HStack { + Text(verbatim: timelineItem.byline) + .font(.footnote) + .foregroundColor(.secondary) + Spacer() + Text(verbatim: timelineItem.dateTimeString) + .font(.footnote) + .foregroundColor(.secondary) + .padding(.trailing, 4) + } + } } Divider() } From 269d9462fea723879578db474721af76c43262d5 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 1 Jul 2020 17:42:44 -0500 Subject: [PATCH 08/10] Change byline to truncate and only use 1 line --- Multiplatform/Shared/Timeline/TimelineItemView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Multiplatform/Shared/Timeline/TimelineItemView.swift b/Multiplatform/Shared/Timeline/TimelineItemView.swift index dd952b9c7..51a63711b 100644 --- a/Multiplatform/Shared/Timeline/TimelineItemView.swift +++ b/Multiplatform/Shared/Timeline/TimelineItemView.swift @@ -23,16 +23,20 @@ struct TimelineItemView: View { } VStack { Text(verbatim: timelineItem.article.title ?? "N/A") + .fontWeight(.semibold) .lineLimit(3) .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) From e7e3a0d5d7c74cea58a4dcaa459f4d9aa29c21b8 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 1 Jul 2020 17:44:45 -0500 Subject: [PATCH 09/10] Change the timeline byline to be the webbed for now --- Multiplatform/Shared/Timeline/TimelineItem.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Multiplatform/Shared/Timeline/TimelineItem.swift b/Multiplatform/Shared/Timeline/TimelineItem.swift index 786854f4b..faf03baa7 100644 --- a/Multiplatform/Shared/Timeline/TimelineItem.swift +++ b/Multiplatform/Shared/Timeline/TimelineItem.swift @@ -34,7 +34,7 @@ struct TimelineItem: Identifiable { } var byline: String { - return article.byline() + return article.webFeed?.nameForDisplay ?? "" } var dateTimeString: String { From ee4d8b34b1476a23adf94fd33567be750c6902f9 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 1 Jul 2020 19:50:40 -0500 Subject: [PATCH 10/10] Make sure image loaders don't loop if onAppear gets repeatedly called --- .../Images/ArticleIconImageLoader.swift | 3 +- .../Shared/Images/FeedIconImageLoader.swift | 37 ++++++++++--------- .../Shared/Timeline/TimelineItemView.swift | 2 +- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Multiplatform/Shared/Images/ArticleIconImageLoader.swift b/Multiplatform/Shared/Images/ArticleIconImageLoader.swift index bae640cc8..ec08f5189 100644 --- a/Multiplatform/Shared/Images/ArticleIconImageLoader.swift +++ b/Multiplatform/Shared/Images/ArticleIconImageLoader.swift @@ -23,6 +23,7 @@ final class ArticleIconImageLoader: ObservableObject { } func loadImage(for article: Article) { + guard image == nil else { return } self.article = article image = article.iconImage() } @@ -50,7 +51,7 @@ private extension ArticleIconImageLoader { for author in authors { if author.avatarURL == avatarURL { - + image = article.iconImage() return } } diff --git a/Multiplatform/Shared/Images/FeedIconImageLoader.swift b/Multiplatform/Shared/Images/FeedIconImageLoader.swift index 4893af107..6a6fa8586 100644 --- a/Multiplatform/Shared/Images/FeedIconImageLoader.swift +++ b/Multiplatform/Shared/Images/FeedIconImageLoader.swift @@ -21,8 +21,27 @@ final class FeedIconImageLoader: 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 FeedIconImageLoader: ObservableObject { } } - -private extension FeedIconImageLoader { - - @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/Timeline/TimelineItemView.swift b/Multiplatform/Shared/Timeline/TimelineItemView.swift index 51a63711b..fd5529d2b 100644 --- a/Multiplatform/Shared/Timeline/TimelineItemView.swift +++ b/Multiplatform/Shared/Timeline/TimelineItemView.swift @@ -24,7 +24,7 @@ struct TimelineItemView: View { VStack { Text(verbatim: timelineItem.article.title ?? "N/A") .fontWeight(.semibold) - .lineLimit(3) + .lineLimit(AppDefaults.timelineNumberOfLines) .frame(maxWidth: .infinity, alignment: .leading) .padding(.trailing, 4) Spacer()