Make timeline filter maintain it state for each timeline individually

This commit is contained in:
Maurice Parker 2020-07-12 16:48:39 -05:00
parent 2ebec7801c
commit d7a4bddc72
5 changed files with 74 additions and 15 deletions

View File

@ -0,0 +1,21 @@
//
// HiddenModifier.swift
// NetNewsWire
//
// Created by Maurice Parker on 7/12/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import SwiftUI
extension View {
func hidden(_ hide: Bool) -> some View {
Group {
if hide {
self.hidden()
} else {
self
}
}
}
}

View File

@ -25,7 +25,8 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
@Published var selectedArticleIDs = Set<String>() // Don't use directly. Use selectedArticles @Published var selectedArticleIDs = Set<String>() // Don't use directly. Use selectedArticles
@Published var selectedArticleID: String? = .none // Don't use directly. Use selectedArticles @Published var selectedArticleID: String? = .none // Don't use directly. Use selectedArticles
@Published var selectedArticles = [Article]() @Published var selectedArticles = [Article]()
@Published var isReadFiltered = false @Published var readFilterEnabledTable = [FeedIdentifier: Bool]()
@Published var isReadFiltered: Bool? = nil
var undoManager: UndoManager? var undoManager: UndoManager?
var undoableCommands = [UndoableCommand]() var undoableCommands = [UndoableCommand]()
@ -33,8 +34,8 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
private var selectedArticleIDsCancellable: AnyCancellable? private var selectedArticleIDsCancellable: AnyCancellable?
private var selectedArticleIDCancellable: AnyCancellable? private var selectedArticleIDCancellable: AnyCancellable?
private var selectedArticlesCancellable: AnyCancellable? private var selectedArticlesCancellable: AnyCancellable?
private var selectedReadFilteredCancellable: AnyCancellable?
private var feeds = [Feed]()
private var fetchSerialNumber = 0 private var fetchSerialNumber = 0
private let fetchRequestQueue = FetchRequestQueue() private let fetchRequestQueue = FetchRequestQueue()
private var exceptionArticleFetcher: ArticleFetcher? private var exceptionArticleFetcher: ArticleFetcher?
@ -97,23 +98,30 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
} }
} }
selectedReadFilteredCancellable = $isReadFiltered.sink { [weak self] filter in
guard let self = self else { return }
self.rebuildTimelineItems(isReadFiltered: filter)
}
} }
// MARK: API // MARK: API
func fetchArticles(feeds: [Feed]) { func fetchArticles(feeds: [Feed]) {
self.feeds = feeds
if feeds.count == 1 { if feeds.count == 1 {
nameForDisplay = feeds.first!.nameForDisplay nameForDisplay = feeds.first!.nameForDisplay
} else { } else {
nameForDisplay = NSLocalizedString("Multiple", comment: "Multiple Feeds") nameForDisplay = NSLocalizedString("Multiple", comment: "Multiple Feeds")
} }
resetReadFilter()
fetchAndReplaceArticlesAsync(feeds: feeds) fetchAndReplaceArticlesAsync(feeds: feeds)
} }
func toggleReadFilter() {
guard let filter = isReadFiltered, let feedID = feeds.first?.feedID else { return }
readFilterEnabledTable[feedID] = !filter
isReadFiltered = !filter
rebuildTimelineItems(isReadFiltered: isReadFiltered)
}
func toggleReadStatusForSelectedArticles() { func toggleReadStatusForSelectedArticles() {
guard !selectedArticles.isEmpty else { guard !selectedArticles.isEmpty else {
return return
@ -207,6 +215,24 @@ private extension TimelineModel {
// MARK: Timeline Management // MARK: Timeline Management
func resetReadFilter() {
guard feeds.count == 1, let timelineFeed = feeds.first else {
isReadFiltered = nil
return
}
guard timelineFeed.defaultReadFilterType != .alwaysRead else {
isReadFiltered = nil
return
}
if let feedID = timelineFeed.feedID, let readFilterEnabled = readFilterEnabledTable[feedID] {
isReadFiltered = readFilterEnabled
} else {
isReadFiltered = timelineFeed.defaultReadFilterType == .read
}
}
func sortParametersDidChange() { func sortParametersDidChange() {
performBlockAndRestoreSelection { performBlockAndRestoreSelection {
let unsortedArticles = Set(articles) let unsortedArticles = Set(articles)
@ -253,7 +279,9 @@ private extension TimelineModel {
// if its been superseded by a newer fetch, or the timeline was emptied, etc., it wont get called. // if its been superseded by a newer fetch, or the timeline was emptied, etc., it wont get called.
precondition(Thread.isMainThread) precondition(Thread.isMainThread)
cancelPendingAsyncFetches() cancelPendingAsyncFetches()
let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: isReadFiltered, representedObjects: representedObjects) { [weak self] (articles, operation) in let filtered = isReadFiltered ?? false
let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: filtered, representedObjects: representedObjects) { [weak self] (articles, operation) in
precondition(Thread.isMainThread) precondition(Thread.isMainThread)
guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else { guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else {
return return
@ -269,11 +297,12 @@ private extension TimelineModel {
// TODO: Update unread counts and other item done in didSet on AppKit // TODO: Update unread counts and other item done in didSet on AppKit
} }
func rebuildTimelineItems(isReadFiltered: Bool) { func rebuildTimelineItems(isReadFiltered: Bool?) {
let filtered = isReadFiltered ?? false
let selectedArticleIDs = selectedArticles.map { $0.articleID } let selectedArticleIDs = selectedArticles.map { $0.articleID }
timelineItems = articles.compactMap { article in timelineItems = articles.compactMap { article in
if isReadFiltered && article.status.read && !selectedArticleIDs.contains(article.articleID) { if filtered && article.status.read && !selectedArticleIDs.contains(article.articleID) {
return nil return nil
} else { } else {
return TimelineItem(article: article) return TimelineItem(article: article)

View File

@ -19,15 +19,17 @@ struct TimelineToolbarModifier: ViewModifier {
ToolbarItem(placement: .navigation) { ToolbarItem(placement: .navigation) {
Button (action: { Button (action: {
withAnimation { withAnimation {
timelineModel.isReadFiltered.toggle() timelineModel.toggleReadFilter()
} }
}, label: { }, label: {
if timelineModel.isReadFiltered { if timelineModel.isReadFiltered ?? false {
AppAssets.filterActiveImage.font(.title3) AppAssets.filterActiveImage.font(.title3)
} else { } else {
AppAssets.filterInactiveImage.font(.title3) AppAssets.filterInactiveImage.font(.title3)
} }
}).help(timelineModel.isReadFiltered ? "Show Read Articles" : "Filter Read Articles") })
.hidden(timelineModel.isReadFiltered == nil)
.help(timelineModel.isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles")
} }
ToolbarItem { ToolbarItem {

View File

@ -20,18 +20,19 @@ struct TimelineView: View {
Spacer() Spacer()
Button (action: { Button (action: {
withAnimation { withAnimation {
timelineModel.isReadFiltered.toggle() timelineModel.toggleReadFilter()
} }
}, label: { }, label: {
if timelineModel.isReadFiltered { if timelineModel.isReadFiltered ?? false {
AppAssets.filterActiveImage AppAssets.filterActiveImage
} else { } else {
AppAssets.filterInactiveImage AppAssets.filterInactiveImage
} }
}) })
.hidden(timelineModel.isReadFiltered == nil)
.padding(.top, 8).padding(.trailing) .padding(.top, 8).padding(.trailing)
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
.help(timelineModel.isReadFiltered ? "Show Read Articles" : "Filter Read Articles") .help(timelineModel.isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles")
} }
ZStack { ZStack {
NavigationLink(destination: ArticleContainerView(articles: timelineModel.selectedArticles), isActive: $navigate) { NavigationLink(destination: ArticleContainerView(articles: timelineModel.selectedArticles), isActive: $navigate) {

View File

@ -296,6 +296,8 @@
5193CD58245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; }; 5193CD58245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; };
5193CD59245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; }; 5193CD59245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; };
5193CD5A245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; }; 5193CD5A245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; };
5194736E24BBB937001A2939 /* HiddenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5194736D24BBB937001A2939 /* HiddenModifier.swift */; };
5194736F24BBB937001A2939 /* HiddenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5194736D24BBB937001A2939 /* HiddenModifier.swift */; };
519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519B8D322143397200FA689C /* SharingServiceDelegate.swift */; }; 519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519B8D322143397200FA689C /* SharingServiceDelegate.swift */; };
519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E743422C663F900A78E47 /* SceneDelegate.swift */; }; 519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E743422C663F900A78E47 /* SceneDelegate.swift */; };
519ED456244828C3007F8E94 /* AddExtensionPointViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */; }; 519ED456244828C3007F8E94 /* AddExtensionPointViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */; };
@ -1960,6 +1962,7 @@
51934CCD2310792F006127BE /* ActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityManager.swift; sourceTree = "<group>"; }; 51934CCD2310792F006127BE /* ActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityManager.swift; sourceTree = "<group>"; };
51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTimelineFeedDelegate.swift; sourceTree = "<group>"; }; 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTimelineFeedDelegate.swift; sourceTree = "<group>"; };
5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RedditFeedProvider-Extensions.swift"; sourceTree = "<group>"; }; 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RedditFeedProvider-Extensions.swift"; sourceTree = "<group>"; };
5194736D24BBB937001A2939 /* HiddenModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenModifier.swift; sourceTree = "<group>"; };
519B8D322143397200FA689C /* SharingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceDelegate.swift; sourceTree = "<group>"; }; 519B8D322143397200FA689C /* SharingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceDelegate.swift; sourceTree = "<group>"; };
519E743422C663F900A78E47 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; }; 519E743422C663F900A78E47 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExtensionPointViewController.swift; sourceTree = "<group>"; }; 519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExtensionPointViewController.swift; sourceTree = "<group>"; };
@ -2687,6 +2690,7 @@
children = ( children = (
514E6C0524AD2B5F00AC6F6E /* Image-Extensions.swift */, 514E6C0524AD2B5F00AC6F6E /* Image-Extensions.swift */,
5181C5AC24AF89B1002E0F70 /* PreferredColorSchemeModifier.swift */, 5181C5AC24AF89B1002E0F70 /* PreferredColorSchemeModifier.swift */,
5194736D24BBB937001A2939 /* HiddenModifier.swift */,
); );
path = "SwiftUI Extensions"; path = "SwiftUI Extensions";
sourceTree = "<group>"; sourceTree = "<group>";
@ -4992,6 +4996,7 @@
FF64D0E724AF53EE0084080A /* RefreshProgressModel.swift in Sources */, FF64D0E724AF53EE0084080A /* RefreshProgressModel.swift in Sources */,
51E4996A24A8762D00B667CB /* ExtractedArticle.swift in Sources */, 51E4996A24A8762D00B667CB /* ExtractedArticle.swift in Sources */,
51919FF124AB864A00541E64 /* TimelineModel.swift in Sources */, 51919FF124AB864A00541E64 /* TimelineModel.swift in Sources */,
5194736E24BBB937001A2939 /* HiddenModifier.swift in Sources */,
51E498F124A8085D00B667CB /* StarredFeedDelegate.swift in Sources */, 51E498F124A8085D00B667CB /* StarredFeedDelegate.swift in Sources */,
51E498FF24A808BB00B667CB /* SingleFaviconDownloader.swift in Sources */, 51E498FF24A808BB00B667CB /* SingleFaviconDownloader.swift in Sources */,
51E4997224A8784300B667CB /* DefaultFeedsImporter.swift in Sources */, 51E4997224A8784300B667CB /* DefaultFeedsImporter.swift in Sources */,
@ -5119,6 +5124,7 @@
51E4993A24A8708800B667CB /* AppDelegate.swift in Sources */, 51E4993A24A8708800B667CB /* AppDelegate.swift in Sources */,
51E498CE24A8085D00B667CB /* UnreadFeed.swift in Sources */, 51E498CE24A8085D00B667CB /* UnreadFeed.swift in Sources */,
51E498C724A8085D00B667CB /* StarredFeedDelegate.swift in Sources */, 51E498C724A8085D00B667CB /* StarredFeedDelegate.swift in Sources */,
5194736F24BBB937001A2939 /* HiddenModifier.swift in Sources */,
51919FB724AABCA100541E64 /* IconImageView.swift in Sources */, 51919FB724AABCA100541E64 /* IconImageView.swift in Sources */,
51B54A6924B54A490014348B /* IconView.swift in Sources */, 51B54A6924B54A490014348B /* IconView.swift in Sources */,
51E498FA24A808BA00B667CB /* SingleFaviconDownloader.swift in Sources */, 51E498FA24A808BA00B667CB /* SingleFaviconDownloader.swift in Sources */,