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 Combine
import RSCore
import Account
@ -19,31 +20,13 @@ class SidebarModel: ObservableObject {
weak var delegate: SidebarModelDelegate?
@Published var sidebarItems = [SidebarItem]()
@Published var selectedFeedIdentifiers = Set<FeedIdentifier>()
@Published var selectedFeedIdentifier: FeedIdentifier? = .none
@Published var selectedFeeds = [Feed]()
#if os(macOS)
@Published var selectedSidebarItems = Set<FeedIdentifier>() {
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
}
}
private var selectedFeedIdentifiersCancellable: AnyCancellable?
private var selectedFeedIdentifierCancellable: AnyCancellable?
init() {
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidInitialize(_:)), name: .UnreadCountDidInitialize, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(containerChildrenDidChange(_:)), name: .ChildrenDidChange, 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(userDidAddAccount(_:)), name: .UserDidAddAccount, 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
@ -86,6 +84,7 @@ class SidebarModel: ObservableObject {
sidebarItems = items
}
}
// MARK: Private

View File

@ -15,27 +15,36 @@ struct SidebarView: View {
// @SceneStorage("expandedContainers") private var expandedContainerData = Data()
@StateObject private var expandedContainers = SidebarExpandedContainers()
@EnvironmentObject private var sidebarModel: SidebarModel
@State var navigate = false
@ViewBuilder
var body: some View {
#if os(macOS)
List(selection: $sidebarModel.selectedSidebarItems) {
containedList
ZStack {
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
List {
containedList
rows
}
#endif
// .onAppear {
// expandedContainers.data = expandedContainerData
// }
// .onReceive(expandedContainers.objectDidChange) {
// expandedContainerData = expandedContainers.data
// }
// .onAppear {
// expandedContainers.data = expandedContainerData
// }
// .onReceive(expandedContainers.objectDidChange) {
// expandedContainerData = expandedContainers.data
// }
}
var containedList: some View {
var rows: some View {
ForEach(sidebarModel.sidebarItems) { sidebarItem in
if let containerID = sidebarItem.containerID {
DisclosureGroup(isExpanded: $expandedContainers[containerID]) {
@ -43,34 +52,13 @@ struct SidebarView: View {
if let containerID = sidebarItem.containerID {
DisclosureGroup(isExpanded: $expandedContainers[containerID]) {
ForEach(sidebarItem.children) { sidebarItem in
ZStack {
SidebarItemView(sidebarItem: sidebarItem)
NavigationLink(destination: (TimelineContainerView(feed: sidebarItem.feed)),
tag: sidebarItem.feed!.feedID!,
selection: $sidebarModel.selectedSidebarItem) {
EmptyView()
}.buttonStyle(PlainButtonStyle())
}
buildSidebarItemNavigation(sidebarItem)
}
} label: {
ZStack {
SidebarItemView(sidebarItem: sidebarItem)
NavigationLink(destination: (TimelineContainerView(feed: sidebarItem.feed)),
tag: sidebarItem.feed!.feedID!,
selection: $sidebarModel.selectedSidebarItem) {
EmptyView()
}.buttonStyle(PlainButtonStyle())
}
buildSidebarItemNavigation(sidebarItem)
}
} else {
ZStack {
SidebarItemView(sidebarItem: sidebarItem)
NavigationLink(destination: (TimelineContainerView(feed: sidebarItem.feed)),
tag: sidebarItem.feed!.feedID!,
selection: $sidebarModel.selectedSidebarItem) {
EmptyView()
}.buttonStyle(PlainButtonStyle())
}
buildSidebarItemNavigation(sidebarItem)
}
}
} 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
@StateObject private var timelineModel = TimelineModel()
var feed: Feed? = nil
var feeds: [Feed]? = nil
@ViewBuilder var body: some View {
if let feed = feed {
if let feeds = feeds {
TimelineView()
.modifier(TimelineTitleModifier(title: feed.nameForDisplay))
.modifier(TimelineTitleModifier(title: timelineModel.nameForDisplay))
.modifier(TimelineToolbarModifier())
.environmentObject(timelineModel)
.onAppear {
sceneModel.timelineModel = timelineModel
timelineModel.delegate = sceneModel
timelineModel.rebuildTimelineItems(feed)
timelineModel.rebuildTimelineItems(feeds: feeds)
}
} else {
EmptyView()

View File

@ -19,9 +19,9 @@ class TimelineModel: ObservableObject {
weak var delegate: TimelineModelDelegate?
@Published var nameForDisplay = ""
@Published var timelineItems = [TimelineItem]()
private var feeds = [Feed]()
private var fetchSerialNumber = 0
private let fetchRequestQueue = FetchRequestQueue()
private var exceptionArticleFetcher: ArticleFetcher?
@ -64,9 +64,13 @@ class TimelineModel: ObservableObject {
// MARK: API
func rebuildTimelineItems(_ feed: Feed) {
feeds = [feed]
fetchAndReplaceArticlesAsync()
func rebuildTimelineItems(feeds: [Feed]) {
if feeds.count == 1 {
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
@ -144,9 +148,7 @@ private extension TimelineModel {
// MARK: Article Fetching
func fetchAndReplaceArticlesAsync() {
cancelPendingAsyncFetches()
func fetchAndReplaceArticlesAsync(feeds: [Feed]) {
var fetchers = feeds as [ArticleFetcher]
if let fetcher = exceptionArticleFetcher {
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.
precondition(Thread.isMainThread)
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)
guard !operation.isCanceled, let strongSelf = self, operation.id == strongSelf.fetchSerialNumber else {
return