Implement Sidebar multiselect for macOS
This commit is contained in:
parent
14109b66a5
commit
360f7a07bf
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 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
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue