Implement Sidebar multiselect for macOS

This commit is contained in:
Maurice Parker 2020-07-11 12:47:13 -05:00
parent 14109b66a5
commit 360f7a07bf
4 changed files with 75 additions and 71 deletions

View File

@ -7,6 +7,7 @@
// //
import Foundation import Foundation
import Combine
import RSCore import RSCore
import Account import Account
@ -19,30 +20,12 @@ class SidebarModel: ObservableObject {
weak var delegate: SidebarModelDelegate? weak var delegate: SidebarModelDelegate?
@Published var sidebarItems = [SidebarItem]() @Published var sidebarItems = [SidebarItem]()
@Published var selectedFeedIdentifiers = Set<FeedIdentifier>()
@Published var selectedFeedIdentifier: FeedIdentifier? = .none
@Published var selectedFeeds = [Feed]()
#if os(macOS) private var selectedFeedIdentifiersCancellable: AnyCancellable?
@Published var selectedSidebarItems = Set<FeedIdentifier>() { private var selectedFeedIdentifierCancellable: AnyCancellable?
didSet {
print(selectedSidebarItems)
}
}
#endif
private var items = Set<FeedIdentifier>()
@Published var selectedSidebarItem: FeedIdentifier? = .none {
willSet {
#if os(macOS)
if newValue != nil {
items.insert(newValue!)
} else {
selectedSidebarItems = items
items.removeAll()
}
#endif
}
}
init() { init() {
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidInitialize(_:)), name: .UnreadCountDidInitialize, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidInitialize(_:)), name: .UnreadCountDidInitialize, object: nil)
@ -52,6 +35,21 @@ class SidebarModel: ObservableObject {
NotificationCenter.default.addObserver(self, selector: #selector(accountStateDidChange(_:)), name: .AccountStateDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountStateDidChange(_:)), name: .AccountStateDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDidAddAccount(_:)), name: .UserDidAddAccount, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(userDidAddAccount(_:)), name: .UserDidAddAccount, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(userDidDeleteAccount(_:)), name: .UserDidDeleteAccount, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(userDidDeleteAccount(_:)), name: .UserDidDeleteAccount, object: nil)
// TODO: This should be rewritten to use Combine correctly
selectedFeedIdentifiersCancellable = $selectedFeedIdentifiers.sink { [weak self] feedIDs in
guard let self = self else { return }
self.selectedFeeds = feedIDs.compactMap { AccountManager.shared.existingFeed(with: $0) }
}
// TODO: This should be rewritten to use Combine correctly
selectedFeedIdentifierCancellable = $selectedFeedIdentifier.sink { [weak self] feedID in
guard let self = self else { return }
if let feedID = feedID, let feed = AccountManager.shared.existingFeed(with: feedID) {
self.selectedFeeds = [feed]
}
}
} }
// MARK: API // MARK: API
@ -86,6 +84,7 @@ class SidebarModel: ObservableObject {
sidebarItems = items sidebarItems = items
} }
} }
// MARK: Private // MARK: Private

View File

@ -15,27 +15,36 @@ struct SidebarView: View {
// @SceneStorage("expandedContainers") private var expandedContainerData = Data() // @SceneStorage("expandedContainers") private var expandedContainerData = Data()
@StateObject private var expandedContainers = SidebarExpandedContainers() @StateObject private var expandedContainers = SidebarExpandedContainers()
@EnvironmentObject private var sidebarModel: SidebarModel @EnvironmentObject private var sidebarModel: SidebarModel
@State var navigate = false
@ViewBuilder @ViewBuilder
var body: some View { var body: some View {
#if os(macOS) #if os(macOS)
List(selection: $sidebarModel.selectedSidebarItems) { ZStack {
containedList NavigationLink(destination: TimelineContainerView(feeds: sidebarModel.selectedFeeds), isActive: $navigate) {
EmptyView()
}.hidden()
List(selection: $sidebarModel.selectedFeedIdentifiers) {
rows
}
}
.onChange(of: sidebarModel.selectedFeedIdentifiers) { value in
navigate = !sidebarModel.selectedFeedIdentifiers.isEmpty
} }
#else #else
List { List {
containedList rows
} }
#endif #endif
// .onAppear { // .onAppear {
// expandedContainers.data = expandedContainerData // expandedContainers.data = expandedContainerData
// } // }
// .onReceive(expandedContainers.objectDidChange) { // .onReceive(expandedContainers.objectDidChange) {
// expandedContainerData = expandedContainers.data // expandedContainerData = expandedContainers.data
// } // }
} }
var containedList: some View { var rows: some View {
ForEach(sidebarModel.sidebarItems) { sidebarItem in ForEach(sidebarModel.sidebarItems) { sidebarItem in
if let containerID = sidebarItem.containerID { if let containerID = sidebarItem.containerID {
DisclosureGroup(isExpanded: $expandedContainers[containerID]) { DisclosureGroup(isExpanded: $expandedContainers[containerID]) {
@ -43,34 +52,13 @@ struct SidebarView: View {
if let containerID = sidebarItem.containerID { if let containerID = sidebarItem.containerID {
DisclosureGroup(isExpanded: $expandedContainers[containerID]) { DisclosureGroup(isExpanded: $expandedContainers[containerID]) {
ForEach(sidebarItem.children) { sidebarItem in ForEach(sidebarItem.children) { sidebarItem in
ZStack { buildSidebarItemNavigation(sidebarItem)
SidebarItemView(sidebarItem: sidebarItem)
NavigationLink(destination: (TimelineContainerView(feed: sidebarItem.feed)),
tag: sidebarItem.feed!.feedID!,
selection: $sidebarModel.selectedSidebarItem) {
EmptyView()
}.buttonStyle(PlainButtonStyle())
}
} }
} label: { } label: {
ZStack { buildSidebarItemNavigation(sidebarItem)
SidebarItemView(sidebarItem: sidebarItem)
NavigationLink(destination: (TimelineContainerView(feed: sidebarItem.feed)),
tag: sidebarItem.feed!.feedID!,
selection: $sidebarModel.selectedSidebarItem) {
EmptyView()
}.buttonStyle(PlainButtonStyle())
}
} }
} else { } else {
ZStack { buildSidebarItemNavigation(sidebarItem)
SidebarItemView(sidebarItem: sidebarItem)
NavigationLink(destination: (TimelineContainerView(feed: sidebarItem.feed)),
tag: sidebarItem.feed!.feedID!,
selection: $sidebarModel.selectedSidebarItem) {
EmptyView()
}.buttonStyle(PlainButtonStyle())
}
} }
} }
} label: { } label: {
@ -80,4 +68,19 @@ struct SidebarView: View {
} }
} }
func buildSidebarItemNavigation(_ sidebarItem: SidebarItem) -> some View {
#if os(macOS)
return SidebarItemView(sidebarItem: sidebarItem).tag(sidebarItem.feed!.feedID!)
#else
return ZStack {
SidebarItemView(sidebarItem: sidebarItem)
NavigationLink(destination: TimelineContainerView(feeds: sidebarModel.selectedFeeds),
tag: sidebarItem.feed!.feedID!,
selection: $sidebarModel.selectedFeedIdentifier) {
EmptyView()
}.buttonStyle(PlainButtonStyle())
}
#endif
}
} }

View File

@ -13,18 +13,18 @@ struct TimelineContainerView: View {
@EnvironmentObject private var sceneModel: SceneModel @EnvironmentObject private var sceneModel: SceneModel
@StateObject private var timelineModel = TimelineModel() @StateObject private var timelineModel = TimelineModel()
var feed: Feed? = nil var feeds: [Feed]? = nil
@ViewBuilder var body: some View { @ViewBuilder var body: some View {
if let feed = feed { if let feeds = feeds {
TimelineView() TimelineView()
.modifier(TimelineTitleModifier(title: feed.nameForDisplay)) .modifier(TimelineTitleModifier(title: timelineModel.nameForDisplay))
.modifier(TimelineToolbarModifier()) .modifier(TimelineToolbarModifier())
.environmentObject(timelineModel) .environmentObject(timelineModel)
.onAppear { .onAppear {
sceneModel.timelineModel = timelineModel sceneModel.timelineModel = timelineModel
timelineModel.delegate = sceneModel timelineModel.delegate = sceneModel
timelineModel.rebuildTimelineItems(feed) timelineModel.rebuildTimelineItems(feeds: feeds)
} }
} else { } else {
EmptyView() EmptyView()

View File

@ -19,9 +19,9 @@ class TimelineModel: ObservableObject {
weak var delegate: TimelineModelDelegate? weak var delegate: TimelineModelDelegate?
@Published var nameForDisplay = ""
@Published var timelineItems = [TimelineItem]() @Published var timelineItems = [TimelineItem]()
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?
@ -64,9 +64,13 @@ class TimelineModel: ObservableObject {
// MARK: API // MARK: API
func rebuildTimelineItems(_ feed: Feed) { func rebuildTimelineItems(feeds: [Feed]) {
feeds = [feed] if feeds.count == 1 {
fetchAndReplaceArticlesAsync() nameForDisplay = feeds.first!.nameForDisplay
} else {
nameForDisplay = NSLocalizedString("Multiple", comment: "Multiple Feeds")
}
fetchAndReplaceArticlesAsync(feeds: feeds)
} }
// TODO: Replace this with ScrollViewReader if we have to keep it // TODO: Replace this with ScrollViewReader if we have to keep it
@ -144,9 +148,7 @@ private extension TimelineModel {
// MARK: Article Fetching // MARK: Article Fetching
func fetchAndReplaceArticlesAsync() { func fetchAndReplaceArticlesAsync(feeds: [Feed]) {
cancelPendingAsyncFetches()
var fetchers = feeds as [ArticleFetcher] var fetchers = feeds as [ArticleFetcher]
if let fetcher = exceptionArticleFetcher { if let fetcher = exceptionArticleFetcher {
fetchers.append(fetcher) fetchers.append(fetcher)
@ -168,7 +170,7 @@ 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 ?? true, representedObjects: representedObjects) { [weak self] (articles, operation) in let fetchOperation = FetchRequestOperation(id: fetchSerialNumber, readFilter: isReadFiltered, 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