Optimized Timeline context menu so that it isn't constantly scanning for article location in array
This commit is contained in:
parent
4bf4c6d6c2
commit
e7a68f433d
@ -15,9 +15,9 @@ struct TimelineContextMenu: View {
|
||||
|
||||
@ViewBuilder var body: some View {
|
||||
|
||||
if timelineModel.canMarkIndicatedArticlesAsRead(timelineItem.article) {
|
||||
if timelineModel.canMarkIndicatedArticlesAsRead(timelineItem) {
|
||||
Button {
|
||||
timelineModel.markIndicatedArticlesAsRead(timelineItem.article)
|
||||
timelineModel.markIndicatedArticlesAsRead(timelineItem)
|
||||
} label: {
|
||||
Text("Mark as Read")
|
||||
#if os(iOS)
|
||||
@ -26,9 +26,9 @@ struct TimelineContextMenu: View {
|
||||
}
|
||||
}
|
||||
|
||||
if timelineModel.canMarkIndicatedArticlesAsUnread(timelineItem.article) {
|
||||
if timelineModel.canMarkIndicatedArticlesAsUnread(timelineItem) {
|
||||
Button {
|
||||
timelineModel.markIndicatedArticlesAsUnread(timelineItem.article)
|
||||
timelineModel.markIndicatedArticlesAsUnread(timelineItem)
|
||||
} label: {
|
||||
Text("Mark as Unread")
|
||||
#if os(iOS)
|
||||
@ -37,9 +37,9 @@ struct TimelineContextMenu: View {
|
||||
}
|
||||
}
|
||||
|
||||
if timelineModel.canMarkIndicatedArticlesAsStarred(timelineItem.article) {
|
||||
if timelineModel.canMarkIndicatedArticlesAsStarred(timelineItem) {
|
||||
Button {
|
||||
timelineModel.markIndicatedArticlesAsStarred(timelineItem.article)
|
||||
timelineModel.markIndicatedArticlesAsStarred(timelineItem)
|
||||
} label: {
|
||||
Text("Mark as Starred")
|
||||
#if os(iOS)
|
||||
@ -48,9 +48,9 @@ struct TimelineContextMenu: View {
|
||||
}
|
||||
}
|
||||
|
||||
if timelineModel.canMarkIndicatedArticlesAsUnstarred(timelineItem.article) {
|
||||
if timelineModel.canMarkIndicatedArticlesAsUnstarred(timelineItem) {
|
||||
Button {
|
||||
timelineModel.markIndicatedArticlesAsUnstarred(timelineItem.article)
|
||||
timelineModel.markIndicatedArticlesAsUnstarred(timelineItem)
|
||||
} label: {
|
||||
Text("Mark as Unstarred")
|
||||
#if os(iOS)
|
||||
@ -59,9 +59,9 @@ struct TimelineContextMenu: View {
|
||||
}
|
||||
}
|
||||
|
||||
if timelineModel.canMarkAboveAsRead(timelineItem.article) {
|
||||
if timelineModel.canMarkAboveAsRead(timelineItem) {
|
||||
Button {
|
||||
timelineModel.markAboveAsRead(timelineItem.article)
|
||||
timelineModel.markAboveAsRead(timelineItem)
|
||||
} label: {
|
||||
Text("Mark Above as Read")
|
||||
#if os(iOS)
|
||||
@ -70,9 +70,9 @@ struct TimelineContextMenu: View {
|
||||
}
|
||||
}
|
||||
|
||||
if timelineModel.canMarkBelowAsRead(timelineItem.article) {
|
||||
if timelineModel.canMarkBelowAsRead(timelineItem) {
|
||||
Button {
|
||||
timelineModel.markBelowAsRead(timelineItem.article)
|
||||
timelineModel.markBelowAsRead(timelineItem)
|
||||
} label: {
|
||||
Text("Mark Below As Read")
|
||||
#if os(iOS)
|
||||
@ -81,22 +81,22 @@ struct TimelineContextMenu: View {
|
||||
}
|
||||
}
|
||||
|
||||
if let feed = timelineItem.article.webFeed, timelineModel.canMarkAllAsReadInFeed(feed) {
|
||||
if timelineModel.canMarkAllAsReadInWebFeed(timelineItem) {
|
||||
Divider()
|
||||
Button {
|
||||
timelineModel.markAllAsReadInFeed(feed)
|
||||
timelineModel.markAllAsReadInWebFeed(timelineItem)
|
||||
} label: {
|
||||
Text("Mark All as Read in “\(feed.nameForDisplay)”")
|
||||
Text("Mark All as Read in “\(timelineItem.article.webFeed?.nameForDisplay ?? "")”")
|
||||
#if os(iOS)
|
||||
AppAssets.markAllAsReadImage
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
if timelineModel.canOpenIndicatedArticleInBrowser(timelineItem.article) {
|
||||
if timelineModel.canOpenIndicatedArticleInBrowser(timelineItem) {
|
||||
Divider()
|
||||
Button {
|
||||
timelineModel.openIndicatedArticleInBrowser(timelineItem.article)
|
||||
timelineModel.openIndicatedArticleInBrowser(timelineItem)
|
||||
} label: {
|
||||
Text("Open in Browser")
|
||||
#if os(iOS)
|
||||
|
@ -18,6 +18,7 @@ enum TimelineItemStatus {
|
||||
struct TimelineItem: Identifiable {
|
||||
|
||||
var id: String
|
||||
var index: Int
|
||||
var article: Article
|
||||
|
||||
var status: TimelineItemStatus = .showNone
|
||||
@ -26,8 +27,9 @@ struct TimelineItem: Identifiable {
|
||||
var byline: String
|
||||
var dateTimeString: String
|
||||
|
||||
init(article: Article) {
|
||||
init(index: Int, article: Article) {
|
||||
self.id = article.articleID
|
||||
self.index = index
|
||||
self.article = article
|
||||
self.byline = article.webFeed?.nameForDisplay ?? ""
|
||||
self.dateTimeString = ArticleStringFormatter.dateString(article.logicalDatePublished)
|
||||
|
@ -27,8 +27,9 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
|
||||
|
||||
@Published var nameForDisplay = ""
|
||||
@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? = nil // Don't use directly. Use selectedArticles
|
||||
@Published var selectedArticles = [Article]()
|
||||
@Published var selectedTimelineItems = [TimelineItem]()
|
||||
@Published var readFilterEnabledTable = [FeedIdentifier: Bool]()
|
||||
@Published var isReadFiltered: Bool? = nil
|
||||
|
||||
@ -157,6 +158,17 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
|
||||
}
|
||||
.assign(to: $selectedArticles)
|
||||
|
||||
// Assign the selected timeline items
|
||||
$selectedArticles.compactMap { [weak self] selectedArticles in
|
||||
return selectedArticles.compactMap {
|
||||
if let index = self?.idToTimelineItemDictionary[$0.articleID] {
|
||||
return self?.timelineItems[index]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}.assign(to: $selectedTimelineItems)
|
||||
|
||||
// Automatically mark a selected record as read
|
||||
$selectedArticles
|
||||
.filter { $0.count == 1 }
|
||||
.compactMap { $0.first }
|
||||
@ -185,13 +197,13 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
|
||||
}
|
||||
}
|
||||
|
||||
func canMarkIndicatedArticlesAsRead(_ article: Article) -> Bool {
|
||||
let articles = indicatedArticles(article)
|
||||
func canMarkIndicatedArticlesAsRead(_ timelineItem: TimelineItem) -> Bool {
|
||||
let articles = indicatedTimelineItems(timelineItem).map { $0.article }
|
||||
return articles.anyArticleIsUnread()
|
||||
}
|
||||
|
||||
func markIndicatedArticlesAsRead(_ article: Article) {
|
||||
let articles = indicatedArticles(article)
|
||||
func markIndicatedArticlesAsRead(_ timelineItem: TimelineItem) {
|
||||
let articles = indicatedTimelineItems(timelineItem).map { $0.article }
|
||||
markArticlesWithUndo(articles, statusKey: .read, flag: true)
|
||||
}
|
||||
|
||||
@ -199,13 +211,13 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
|
||||
markArticlesWithUndo(selectedArticles, statusKey: .read, flag: true)
|
||||
}
|
||||
|
||||
func canMarkIndicatedArticlesAsUnread(_ article: Article) -> Bool {
|
||||
let articles = indicatedArticles(article)
|
||||
func canMarkIndicatedArticlesAsUnread(_ timelineItem: TimelineItem) -> Bool {
|
||||
let articles = indicatedTimelineItems(timelineItem).map { $0.article }
|
||||
return articles.anyArticleIsReadAndCanMarkUnread()
|
||||
}
|
||||
|
||||
func markIndicatedArticlesAsUnread(_ article: Article) {
|
||||
let articles = indicatedArticles(article)
|
||||
func markIndicatedArticlesAsUnread(_ timelineItem: TimelineItem) {
|
||||
let articles = indicatedTimelineItems(timelineItem).map { $0.article }
|
||||
markArticlesWithUndo(articles, statusKey: .read, flag: false)
|
||||
}
|
||||
|
||||
@ -213,36 +225,36 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
|
||||
markArticlesWithUndo(selectedArticles, statusKey: .read, flag: false)
|
||||
}
|
||||
|
||||
func canMarkAboveAsRead(_ article: Article) -> Bool {
|
||||
let article = indicatedAboveArticle(article)
|
||||
return articles.articlesAbove(article: article).canMarkAllAsRead()
|
||||
func canMarkAboveAsRead(_ timelineItem: TimelineItem) -> Bool {
|
||||
let timelineItem = indicatedAboveTimelineItem(timelineItem)
|
||||
return articles.articlesAbove(position: timelineItem.index).canMarkAllAsRead()
|
||||
}
|
||||
|
||||
func markAboveAsRead(_ article: Article) {
|
||||
let article = indicatedAboveArticle(article)
|
||||
let articlesToMark = articles.articlesAbove(article: article)
|
||||
func markAboveAsRead(_ timelineItem: TimelineItem) {
|
||||
let timelineItem = indicatedAboveTimelineItem(timelineItem)
|
||||
let articlesToMark = articles.articlesAbove(position: timelineItem.index)
|
||||
guard !articlesToMark.isEmpty else { return }
|
||||
markArticlesWithUndo(articlesToMark, statusKey: .read, flag: true)
|
||||
}
|
||||
|
||||
func canMarkBelowAsRead(_ article: Article) -> Bool {
|
||||
let article = indicatedBelowArticle(article)
|
||||
return articles.articlesBelow(article: article).canMarkAllAsRead()
|
||||
func canMarkBelowAsRead(_ timelineItem: TimelineItem) -> Bool {
|
||||
let timelineItem = indicatedBelowTimelineItem(timelineItem)
|
||||
return articles.articlesBelow(position: timelineItem.index).canMarkAllAsRead()
|
||||
}
|
||||
|
||||
func markBelowAsRead(_ article: Article) {
|
||||
let article = indicatedBelowArticle(article)
|
||||
let articlesToMark = articles.articlesBelow(article: article)
|
||||
func markBelowAsRead(_ timelineItem: TimelineItem) {
|
||||
let timelineItem = indicatedBelowTimelineItem(timelineItem)
|
||||
let articlesToMark = articles.articlesBelow(position: timelineItem.index)
|
||||
guard !articlesToMark.isEmpty else { return }
|
||||
markArticlesWithUndo(articlesToMark, statusKey: .read, flag: true)
|
||||
}
|
||||
|
||||
func canMarkAllAsReadInFeed(_ feed: Feed) -> Bool {
|
||||
return feed.unreadCount > 0
|
||||
func canMarkAllAsReadInWebFeed(_ timelineItem: TimelineItem) -> Bool {
|
||||
return timelineItem.article.webFeed?.unreadCount ?? 0 > 0
|
||||
}
|
||||
|
||||
func markAllAsReadInFeed(_ feed: Feed) {
|
||||
guard let articlesSet = try? feed.fetchArticles() else { return }
|
||||
func markAllAsReadInWebFeed(_ timelineItem: TimelineItem) {
|
||||
guard let articlesSet = try? timelineItem.article.webFeed?.fetchArticles() else { return }
|
||||
let articlesToMark = Array(articlesSet)
|
||||
markArticlesWithUndo(articlesToMark, statusKey: .read, flag: true)
|
||||
}
|
||||
@ -266,13 +278,13 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
|
||||
}
|
||||
}
|
||||
|
||||
func canMarkIndicatedArticlesAsStarred(_ article: Article) -> Bool {
|
||||
let articles = indicatedArticles(article)
|
||||
func canMarkIndicatedArticlesAsStarred(_ timelineItem: TimelineItem) -> Bool {
|
||||
let articles = indicatedTimelineItems(timelineItem).map { $0.article }
|
||||
return articles.anyArticleIsUnstarred()
|
||||
}
|
||||
|
||||
func markIndicatedArticlesAsStarred(_ article: Article) {
|
||||
let articles = indicatedArticles(article)
|
||||
func markIndicatedArticlesAsStarred(_ timelineItem: TimelineItem) {
|
||||
let articles = indicatedTimelineItems(timelineItem).map { $0.article }
|
||||
markArticlesWithUndo(articles, statusKey: .starred, flag: true)
|
||||
}
|
||||
|
||||
@ -280,13 +292,13 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
|
||||
markArticlesWithUndo(selectedArticles, statusKey: .starred, flag: true)
|
||||
}
|
||||
|
||||
func canMarkIndicatedArticlesAsUnstarred(_ article: Article) -> Bool {
|
||||
let articles = indicatedArticles(article)
|
||||
func canMarkIndicatedArticlesAsUnstarred(_ timelineItem: TimelineItem) -> Bool {
|
||||
let articles = indicatedTimelineItems(timelineItem).map { $0.article }
|
||||
return articles.anyArticleIsStarred()
|
||||
}
|
||||
|
||||
func markIndicatedArticlesAsUnstarred(_ article: Article) {
|
||||
let articles = indicatedArticles(article)
|
||||
func markIndicatedArticlesAsUnstarred(_ timelineItem: TimelineItem) {
|
||||
let articles = indicatedTimelineItems(timelineItem).map { $0.article }
|
||||
markArticlesWithUndo(articles, statusKey: .starred, flag: false)
|
||||
}
|
||||
|
||||
@ -294,9 +306,13 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
|
||||
markArticlesWithUndo(selectedArticles, statusKey: .starred, flag: false)
|
||||
}
|
||||
|
||||
func canOpenIndicatedArticleInBrowser(_ article: Article) -> Bool {
|
||||
guard indicatedArticles(article).count == 1 else { return false }
|
||||
return article.preferredLink != nil
|
||||
func canOpenIndicatedArticleInBrowser(_ timelineItem: TimelineItem) -> Bool {
|
||||
guard indicatedTimelineItems(timelineItem).count == 1 else { return false }
|
||||
return timelineItem.article.preferredLink != nil
|
||||
}
|
||||
|
||||
func openIndicatedArticleInBrowser(_ timelineItem: TimelineItem) {
|
||||
openIndicatedArticleInBrowser(timelineItem.article)
|
||||
}
|
||||
|
||||
func openIndicatedArticleInBrowser(_ article: Article) {
|
||||
@ -362,27 +378,27 @@ class TimelineModel: ObservableObject, UndoableCommandRunner {
|
||||
|
||||
private extension TimelineModel {
|
||||
|
||||
func indicatedArticles(_ article: Article) -> [Article] {
|
||||
if selectedArticles.contains(article) {
|
||||
return selectedArticles
|
||||
func indicatedTimelineItems(_ timelineItem: TimelineItem) -> [TimelineItem] {
|
||||
if selectedTimelineItems.contains(where: { $0.id == timelineItem.id }) {
|
||||
return selectedTimelineItems
|
||||
} else {
|
||||
return [article]
|
||||
return [timelineItem]
|
||||
}
|
||||
}
|
||||
|
||||
func indicatedAboveArticle(_ article: Article) -> Article {
|
||||
if selectedArticles.contains(article) {
|
||||
return selectedArticles.first!
|
||||
func indicatedAboveTimelineItem(_ timelineItem: TimelineItem) -> TimelineItem {
|
||||
if selectedTimelineItems.contains(where: { $0.id == timelineItem.id }) {
|
||||
return selectedTimelineItems.first!
|
||||
} else {
|
||||
return article
|
||||
return timelineItem
|
||||
}
|
||||
}
|
||||
|
||||
func indicatedBelowArticle(_ article: Article) -> Article {
|
||||
if selectedArticles.contains(article) {
|
||||
return selectedArticles.last!
|
||||
func indicatedBelowTimelineItem(_ timelineItem: TimelineItem) -> TimelineItem {
|
||||
if selectedTimelineItems.contains(where: { $0.id == timelineItem.id }) {
|
||||
return selectedTimelineItems.last!
|
||||
} else {
|
||||
return article
|
||||
return timelineItem
|
||||
}
|
||||
}
|
||||
|
||||
@ -521,7 +537,11 @@ private extension TimelineModel {
|
||||
}
|
||||
|
||||
func rebuildTimelineItems() {
|
||||
timelineItems = articles.map { TimelineItem(article: $0) }
|
||||
var items = [TimelineItem]()
|
||||
for (index, article) in articles.enumerated() {
|
||||
items.append(TimelineItem(index: index, article: article))
|
||||
}
|
||||
timelineItems = items
|
||||
}
|
||||
|
||||
func queueFetchAndMergeArticles() {
|
||||
|
@ -101,28 +101,30 @@ extension Array where Element == Article {
|
||||
}
|
||||
|
||||
func articlesAbove(article: Article) -> [Article] {
|
||||
guard let position = firstIndex(of: article) else {
|
||||
return []
|
||||
}
|
||||
|
||||
guard let position = firstIndex(of: article) else { return [] }
|
||||
return articlesAbove(position: position)
|
||||
}
|
||||
|
||||
func articlesAbove(position: Int) -> [Article] {
|
||||
guard position < count else { return [] }
|
||||
let articlesAbove = self[..<position]
|
||||
return Array(articlesAbove)
|
||||
}
|
||||
|
||||
func articlesBelow(article: Article) -> [Article] {
|
||||
guard let position = firstIndex(of: article) else {
|
||||
return []
|
||||
}
|
||||
|
||||
guard let position = firstIndex(of: article) else { return [] }
|
||||
return articlesBelow(position: position)
|
||||
}
|
||||
|
||||
func articlesBelow(position: Int) -> [Article] {
|
||||
guard position < count else { return [] }
|
||||
var articlesBelow = Array(self[position...])
|
||||
|
||||
guard !articlesBelow.isEmpty else {
|
||||
return []
|
||||
}
|
||||
|
||||
articlesBelow.removeFirst()
|
||||
|
||||
return articlesBelow
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user