summaryrefslogtreecommitdiff
path: root/Sora
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-18 12:26:07 -0800
committerFuwn <[email protected]>2026-02-18 12:26:11 -0800
commit314ebb285d11caad11482885769402df80c45391 (patch)
tree67a8a254e3d6bac23501134b08d6daf5d14710a8 /Sora
parentperf: memoize generic list derived data and folder grouping (diff)
downloadsora-testing-314ebb285d11caad11482885769402df80c45391.tar.xz
sora-testing-314ebb285d11caad11482885769402df80c45391.zip
perf: eliminate repeated favorites sort map and grid recompute
Diffstat (limited to 'Sora')
-rw-r--r--Sora/Views/FavoritesView.swift247
-rw-r--r--Sora/Views/Shared/ThumbnailGridView.swift45
2 files changed, 173 insertions, 119 deletions
diff --git a/Sora/Views/FavoritesView.swift b/Sora/Views/FavoritesView.swift
index 7c41a2d..6295b23 100644
--- a/Sora/Views/FavoritesView.swift
+++ b/Sora/Views/FavoritesView.swift
@@ -1,7 +1,6 @@
// swiftlint:disable file_length
import SwiftUI
-import WaterfallGrid
struct FavoritesView: View { // swiftlint:disable:this type_body_length
@EnvironmentObject var settings: SettingsManager
@@ -22,64 +21,59 @@ struct FavoritesView: View { // swiftlint:disable:this type_body_length
@State private var navigationPath = NavigationPath()
@State private var isDetailsSheetPresented = false
@State private var selectedFavoriteForDetails: SettingsFavoritePost?
+ @State private var folderHierarchy = FolderHierarchy(folders: [])
+ @State private var displayedFavorites: [SettingsFavoritePost] = []
+ @State private var displayedPosts: [BooruPost] = []
+ @State private var displayedColumnsData: [[SettingsFavoritePost]] = []
- var filteredFavorites: [SettingsFavoritePost] {
- settings.favorites.filter { favorite in
- let matchesFolder: Bool = {
- switch selectedCollectionOption {
- case .all:
- return true
+ private func refreshFolderHierarchy() {
+ folderHierarchy = FolderHierarchy(folders: settings.folders)
+ }
- case .uncategorized:
- return favorite.folder == nil
+ private func matchesSelectedCollection(for favorite: SettingsFavoritePost) -> Bool {
+ switch selectedCollectionOption {
+ case .all:
+ return true
- case .folder(let folderId):
- return favorite.folder == folderId
+ case .uncategorized:
+ return favorite.folder == nil
- case .topLevelFolder(let topLevelName):
- guard let favoriteFolderId = favorite.folder,
- let favoriteFolder = settings.folders.first(where: { $0.id == favoriteFolderId })
- else {
- return false
- }
+ case .folder(let folderIdentifier):
+ return favorite.folder == folderIdentifier
- return favoriteFolder.topLevelName == topLevelName
+ case .topLevelFolder(let topLevelName):
+ guard let favoriteFolderIdentifier = favorite.folder,
+ let favoriteFolder = folderHierarchy.folder(for: favoriteFolderIdentifier)
+ else {
+ return false
+ }
- case .topLevelUncategorized(let topLevelName):
- guard let favoriteFolderId = favorite.folder,
- let favoriteFolder = settings.folders.first(where: { $0.id == favoriteFolderId })
- else {
- return false
- }
+ return favoriteFolder.topLevelName == topLevelName
- return favoriteFolder.name == topLevelName
- }
- }()
- let matchesSearch =
- searchText.isEmpty
- || favorite.tags
- .joined(separator: " ")
- .lowercased()
- .contains(searchText.lowercased())
- let matchesProvider =
- selectedProvider == nil
- || favorite.provider == selectedProvider
+ case .topLevelUncategorized(let topLevelName):
+ guard let favoriteFolderIdentifier = favorite.folder,
+ let favoriteFolder = folderHierarchy.folder(for: favoriteFolderIdentifier)
+ else {
+ return false
+ }
- return matchesFolder && matchesSearch && matchesProvider
+ return favoriteFolder.name == topLevelName
}
}
- var sortedFilteredFavorites: [SettingsFavoritePost] {
- filteredFavorites.sorted { (lhs: SettingsFavoritePost, rhs: SettingsFavoritePost) in
+ private func sortedFavorites(from unsortedFavorites: [SettingsFavoritePost])
+ -> [SettingsFavoritePost]
+ {
+ unsortedFavorites.sorted { leftFavorite, rightFavorite in
switch sort {
case .dateAdded:
- return lhs.date > rhs.date
+ return leftFavorite.date > rightFavorite.date
case .lastVisited:
- return lhs.lastVisit > rhs.lastVisit
+ return leftFavorite.lastVisit > rightFavorite.lastVisit
case .visitCount:
- return lhs.visitedCount > rhs.visitedCount
+ return leftFavorite.visitedCount > rightFavorite.visitedCount
case .shuffle:
return Bool.random()
@@ -87,6 +81,41 @@ struct FavoritesView: View { // swiftlint:disable:this type_body_length
}
}
+ private func columnsData(
+ for favorites: [SettingsFavoritePost], columnCount: Int
+ ) -> [[SettingsFavoritePost]] {
+ (0..<columnCount).map { columnIndex in
+ favorites.enumerated().compactMap { index, favorite in
+ index % columnCount == columnIndex ? favorite : nil
+ }
+ }
+ }
+
+ private func refreshDisplayedFavorites() {
+ let normalizedSearchText = searchText.lowercased()
+ let filteredFavorites = settings.favorites.filter { favorite in
+ let matchesSearch =
+ normalizedSearchText.isEmpty
+ || favorite.tags
+ .joined(separator: " ")
+ .lowercased()
+ .contains(normalizedSearchText)
+ let matchesProvider =
+ selectedProvider == nil
+ || favorite.provider == selectedProvider
+
+ return matchesSelectedCollection(for: favorite) && matchesSearch && matchesProvider
+ }
+ let sortedFavorites = sortedFavorites(from: filteredFavorites)
+
+ displayedFavorites = sortedFavorites
+ displayedPosts = sortedFavorites.map { $0.toBooruPost() }
+ displayedColumnsData = columnsData(
+ for: sortedFavorites,
+ columnCount: settings.thumbnailGridColumns
+ )
+ }
+
private func isProviderFavorited(_ provider: BooruProvider) -> Bool {
settings.favorites.contains { $0.provider == provider }
}
@@ -124,18 +153,11 @@ struct FavoritesView: View { // swiftlint:disable:this type_body_length
.tag(CollectionPickerOption.uncategorized)
}
- ForEach(settings.folders.filter { $0.topLevelName == nil }, id: \.id) { folder in
+ ForEach(folderHierarchy.rootFolders, id: \.id) { folder in
Text(folder.name).tag(CollectionPickerOption.folder(folder.id))
}
- let topLevelFolders = settings.folders
- .reduce(into: [String: [SettingsFolder]]()) { result, folder in
- guard let topLevelName = folder.topLevelName else { return }
-
- result[topLevelName, default: []].append(folder)
- }
-
- ForEach(topLevelFolders.keys.sorted(), id: \.self) { topLevelName in
+ ForEach(folderHierarchy.sortedTopLevelNames, id: \.self) { topLevelName in
Menu {
Button(action: {
selectedCollectionOption = .topLevelFolder(topLevelName)
@@ -143,7 +165,7 @@ struct FavoritesView: View { // swiftlint:disable:this type_body_length
Text("All \(topLevelName)")
}
- if settings.folders.contains(where: { $0.name == topLevelName }) {
+ if folderHierarchy.hasTopLevelUncategorized(forTopLevelName: topLevelName) {
Button(action: {
selectedCollectionOption = .topLevelUncategorized(topLevelName)
}) {
@@ -153,7 +175,10 @@ struct FavoritesView: View { // swiftlint:disable:this type_body_length
Divider()
- ForEach(topLevelFolders[topLevelName] ?? [], id: \.id) { folder in
+ ForEach(
+ folderHierarchy.folders(forTopLevelName: topLevelName),
+ id: \.id
+ ) { folder in
Button(action: {
selectedCollectionOption = .folder(folder.id)
}) {
@@ -185,7 +210,7 @@ struct FavoritesView: View { // swiftlint:disable:this type_body_length
#endif
ScrollView {
- if filteredFavorites.isEmpty,
+ if displayedFavorites.isEmpty,
!searchText.isEmpty || selectedCollectionOption != .all
{
ContentUnavailableView(
@@ -254,7 +279,7 @@ struct FavoritesView: View { // swiftlint:disable:this type_body_length
}
}
- ForEach(settings.folders.filter { $0.topLevelName == nil }, id: \.id) { folder in
+ ForEach(folderHierarchy.rootFolders, id: \.id) { folder in
Button(action: {
selectedCollectionOption = .folder(folder.id)
}) {
@@ -262,14 +287,7 @@ struct FavoritesView: View { // swiftlint:disable:this type_body_length
}
}
- let topLevelFolders = settings.folders
- .reduce(into: [String: [SettingsFolder]]()) { result, folder in
- guard let topLevelName = folder.topLevelName else { return }
-
- result[topLevelName, default: []].append(folder)
- }
-
- ForEach(topLevelFolders.keys.sorted(), id: \.self) { topLevelName in
+ ForEach(folderHierarchy.sortedTopLevelNames, id: \.self) { topLevelName in
Menu {
Button(action: {
selectedCollectionOption = .topLevelFolder(topLevelName)
@@ -277,7 +295,7 @@ struct FavoritesView: View { // swiftlint:disable:this type_body_length
Label("All \(topLevelName)", systemImage: "folder.fill")
}
- if settings.folders.contains(where: { $0.name == topLevelName }) {
+ if folderHierarchy.hasTopLevelUncategorized(forTopLevelName: topLevelName) {
Button(action: {
selectedCollectionOption = .topLevelUncategorized(topLevelName)
}) {
@@ -287,7 +305,10 @@ struct FavoritesView: View { // swiftlint:disable:this type_body_length
Divider()
- ForEach(topLevelFolders[topLevelName] ?? [], id: \.id) { folder in
+ ForEach(
+ folderHierarchy.folders(forTopLevelName: topLevelName),
+ id: \.id
+ ) { folder in
Button(action: {
selectedCollectionOption = .folder(folder.id)
}) {
@@ -327,6 +348,32 @@ struct FavoritesView: View { // swiftlint:disable:this type_body_length
}
#endif
}
+ .onAppear {
+ refreshFolderHierarchy()
+ refreshDisplayedFavorites()
+ }
+ .onChange(of: settings.favorites) {
+ refreshDisplayedFavorites()
+ }
+ .onChange(of: searchText) {
+ refreshDisplayedFavorites()
+ }
+ .onChange(of: selectedCollectionOption) {
+ refreshDisplayedFavorites()
+ }
+ .onChange(of: selectedProvider) {
+ refreshDisplayedFavorites()
+ }
+ .onChange(of: sort) {
+ refreshDisplayedFavorites()
+ }
+ .onChange(of: settings.folders) {
+ refreshFolderHierarchy()
+ refreshDisplayedFavorites()
+ }
+ .onChange(of: settings.thumbnailGridColumns) {
+ refreshDisplayedFavorites()
+ }
.alert(
"Are you sure you want to remove all favorites? This action cannot be undone.",
isPresented: $isShowingRemoveAllConfirmation
@@ -456,55 +503,24 @@ struct FavoritesView: View { // swiftlint:disable:this type_body_length
@ViewBuilder
private func gridView(columnCount: Int) -> some View {
- if settings.alternativeThumbnailGrid {
- let columnsData = getColumnsData(columnCount: columnCount)
-
- HStack(alignment: .top) {
- ForEach(0..<columnCount, id: \.self) { columnIndex in
- LazyVStack {
- ForEach(columnsData[columnIndex], id: \.id) { favorite in
- favoriteGridContent(favorite: favorite)
- .id(favorite.id)
- }
- }
- .transaction { $0.animation = nil }
- }
- }
- #if os(macOS)
- .padding(8)
- #else
- .padding(.horizontal)
- #endif
- .transition(.opacity)
- } else {
- WaterfallGrid(sortedFilteredFavorites, id: \.id) { favorite in
- favoriteGridContent(favorite: favorite)
- .id(favorite.id)
- }
- .gridStyle(columns: columnCount)
- .transaction { $0.animation = nil }
- #if os(macOS)
- .padding(8)
- #else
- .padding(.horizontal)
- #endif
- .transition(.opacity)
- }
- }
-
- private func getColumnsData(columnCount: Int) -> [[SettingsFavoritePost]] {
- (0..<columnCount).map { columnIndex in
- sortedFilteredFavorites.enumerated().compactMap { index, favorite in
- index % columnCount == columnIndex ? favorite : nil
- }
+ ThumbnailGridView(
+ items: displayedFavorites,
+ columnCount: columnCount,
+ useAlternativeGrid: settings.alternativeThumbnailGrid,
+ columnsData: displayedColumnsData
+ ) { favorite in
+ favoriteGridContent(favorite: favorite, posts: displayedPosts)
}
}
- private func favoriteGridContent(favorite: SettingsFavoritePost) -> some View {
+ private func favoriteGridContent(
+ favorite: SettingsFavoritePost,
+ posts: [BooruPost]
+ ) -> some View {
NavigationLink(
value: PostWithContext(
post: favorite.toBooruPost(),
- posts: sortedFilteredFavorites.map { $0.toBooruPost() },
+ posts: posts,
baseSearchText: nil
)
) {
@@ -540,7 +556,7 @@ struct FavoritesView: View { // swiftlint:disable:this type_body_length
}
Menu {
- ForEach(settings.folders.filter { $0.topLevelName == nil }, id: \.id) { folder in
+ ForEach(folderHierarchy.rootFolders, id: \.id) { folder in
Button(action: {
settings.updateFavoriteFolder(withID: favorite.id, folder: folder.id)
}) {
@@ -549,16 +565,9 @@ struct FavoritesView: View { // swiftlint:disable:this type_body_length
.disabled(favorite.folder == folder.id)
}
- let topLevelFolders = settings.folders
- .reduce(into: [String: [SettingsFolder]]()) { result, folder in
- guard let topLevelName = folder.topLevelName else { return }
-
- result[topLevelName, default: []].append(folder)
- }
-
- ForEach(topLevelFolders.keys.sorted(), id: \.self) { topLevelName in
+ ForEach(folderHierarchy.sortedTopLevelNames, id: \.self) { topLevelName in
Menu {
- let topLevelFolder = settings.folders.first { $0.name == topLevelName }
+ let topLevelFolder = folderHierarchy.rootFolders.first { $0.name == topLevelName }
if let topLevelFolder {
Button(action: {
@@ -580,7 +589,7 @@ struct FavoritesView: View { // swiftlint:disable:this type_body_length
Divider()
- ForEach(topLevelFolders[topLevelName] ?? [], id: \.id) { folder in
+ ForEach(folderHierarchy.folders(forTopLevelName: topLevelName), id: \.id) { folder in
Button(action: {
settings.updateFavoriteFolder(withID: favorite.id, folder: folder.id)
}) {
diff --git a/Sora/Views/Shared/ThumbnailGridView.swift b/Sora/Views/Shared/ThumbnailGridView.swift
new file mode 100644
index 0000000..a7130c2
--- /dev/null
+++ b/Sora/Views/Shared/ThumbnailGridView.swift
@@ -0,0 +1,45 @@
+import SwiftUI
+import WaterfallGrid
+
+struct ThumbnailGridView<Item: Hashable & Identifiable, Content: View>: View {
+ let items: [Item]
+ let columnCount: Int
+ let useAlternativeGrid: Bool
+ let columnsData: [[Item]]
+ let content: (Item) -> Content
+
+ var body: some View {
+ if useAlternativeGrid {
+ HStack(alignment: .top) {
+ ForEach(0..<columnCount, id: \.self) { columnIndex in
+ LazyVStack {
+ ForEach(columnsData[columnIndex], id: \.id) { item in
+ content(item)
+ .id(item.id)
+ }
+ }
+ .transaction { $0.animation = nil }
+ }
+ }
+ #if os(macOS)
+ .padding(8)
+ #else
+ .padding(.horizontal)
+ #endif
+ .transition(.opacity)
+ } else {
+ WaterfallGrid(items, id: \.id) { item in
+ content(item)
+ .id(item.id)
+ }
+ .gridStyle(columns: columnCount)
+ .transaction { $0.animation = nil }
+ #if os(macOS)
+ .padding(8)
+ #else
+ .padding(.horizontal)
+ #endif
+ .transition(.opacity)
+ }
+ }
+}