diff options
| author | Fuwn <[email protected]> | 2026-02-18 12:26:07 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-18 12:26:11 -0800 |
| commit | 314ebb285d11caad11482885769402df80c45391 (patch) | |
| tree | 67a8a254e3d6bac23501134b08d6daf5d14710a8 /Sora | |
| parent | perf: memoize generic list derived data and folder grouping (diff) | |
| download | sora-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.swift | 247 | ||||
| -rw-r--r-- | Sora/Views/Shared/ThumbnailGridView.swift | 45 |
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) + } + } +} |