Implement Sidebar multiselect for macOS
This commit is contained in:
parent
14109b66a5
commit
360f7a07bf
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 it’s been superseded by a newer fetch, or the timeline was emptied, etc., it won’t get called.
|
// if it’s been superseded by a newer fetch, or the timeline was emptied, etc., it won’t 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
|
||||||
|
|
Loading…
Reference in New Issue