import SwiftUI import WaterfallGrid struct PostGridView: View { @EnvironmentObject var settings: SettingsManager @EnvironmentObject var manager: BooruManager @State private var isSearchHistoryPresented = false @Binding var selectedTab: Int @State private var viewStates: [UUID: PostGridViewState] = [:] @Environment(\.isSearching) private var isSearching var filteredPosts: [BooruPost] { manager.posts .filter { settings.displayRatings.contains($0.rating) } } var body: some View { ScrollViewReader { _ in ZStack { ForEach(Array(manager.searchHistory.enumerated()), id: \.element.id) { index, query in let isActive = index == manager.historyIndex let filteredPosts = viewStates[query.id]?.posts .filter { settings.displayRatings.contains($0.rating) } ?? [] ScrollView { if filteredPosts.isEmpty && isActive { ProgressView() .padding() } WaterfallGrid(filteredPosts, id: \.id) { post in waterfallGridContent(post: post) .id(post.id) } .gridStyle(columns: settings.thumbnailGridColumns) .padding(8) } .opacity(isActive ? 1 : 0) .frame(height: isActive ? nil : 0) .clipped() } } .searchable(text: $manager.searchText, prompt: "Tags") .searchSuggestions { if settings.searchSuggestionsMode != .disabled { SearchSuggestionsView( items: searchSuggestionsItems(), searchText: $manager.searchText ) } } .onSubmit(of: .search) { manager.performSearch(settings: settings) } .navigationDestination(for: BooruPost.self) { post in PostDetailsView(post: post) } .onChange(of: manager.searchText) { _, _ in if manager.searchText.isEmpty, !isSearching, !manager.isNavigatingHistory { manager.performSearch() } } .onChange(of: manager.posts) { _, newPosts in if manager.historyIndex >= 0 && manager.historyIndex < manager.searchHistory.count { let queryID = manager.searchHistory[manager.historyIndex].id updateViewState(for: queryID, posts: newPosts) } } .onChange(of: manager.currentPage) { _, newPage in if manager.historyIndex >= 0 && manager.historyIndex < manager.searchHistory.count { let queryID = manager.searchHistory[manager.historyIndex].id updateViewState(for: queryID, currentPage: newPage) } } .onChange(of: manager.historyIndex) { _, newIndex in guard newIndex >= 0 && newIndex < manager.searchHistory.count else { return } let queryID = manager.searchHistory[newIndex].id if let state = viewStates[queryID] { manager.posts = state.posts manager.currentPage = state.currentPage } else { manager.posts = [] manager.currentPage = 1 manager.fetchPosts( page: 1, tags: manager.searchHistory[newIndex].tags, replace: true ) } } .toolbar { #if os(macOS) ToolbarItem { Button(action: { Task { await manager.fetchPosts(page: 1, tags: manager.tags, replace: true) } }) { Label("Refresh", systemImage: "arrow.clockwise") } .disabled(manager.isLoading) } #endif #if !os(macOS) ToolbarItem(placement: .bottomBar) { Button(action: { Task { isSearchHistoryPresented.toggle() } }) { Label("Search History", systemImage: "clock.arrow.circlepath") } } if manager.isLoading || manager.isNavigatingHistory { ToolbarItem { ProgressView() } } #endif PlatformSpecificToolbarItem { Button(action: { Task { manager.loadNextPage() } }) { Label( "Manually Load Next Page", systemImage: "arrow.down.to.line" ) } .disabled(manager.isLoading || manager.endOfData) } PlatformSpecificToolbarItem { PostGridBookmarkButtonView() .disabled(manager.tags.isEmpty) } PlatformSpecificToolbarItem { Button(action: { manager.goBackInHistory() }) { Label("Previous Search", systemImage: "chevron.left") } .disabled(!manager.canGoBackInHistory) } PlatformSpecificToolbarItem { Button(action: { manager.goForwardInHistory() }) { Label("Next Search", systemImage: "chevron.right") } .disabled(!manager.canGoForwardInHistory) } } .navigationTitle("Posts") .refreshable { manager.clearCachedPages() manager.fetchPosts(page: 1, tags: manager.tags, replace: true) } .scrollDisabled(manager.isLoading) .sheet(isPresented: $isSearchHistoryPresented) { PostGridSearchHistoryView( selectedTab: $selectedTab, isPresented: $isSearchHistoryPresented ) .frame(minHeight: 250) } #if os(iOS) .gesture( DragGesture() .onEnded { value in if value.startLocation.x < 50 && value.translation.width > 100 { manager.goBackInHistory() debugPrint("ContentView: Swipe left, \(manager.searchHistory)") } if value.startLocation.x > (UIScreen.main.bounds.width - 50) && value.translation.width < -100 { manager.goForwardInHistory() debugPrint("ContentView: Swipe right, \(manager.searchHistory)") } } ) #endif } } private func waterfallGridContent(post: BooruPost) -> some View { Button { if !manager.isLoading { manager.selectedPost = post } } label: { PostGridThumbnailView(post: post, posts: filteredPosts) } .buttonStyle(PlainButtonStyle()) .contextMenu { Button(action: { manager.selectedPost = post }) { Label("Select Post", systemImage: "arrow.right.circle") } } } private func searchSuggestionsItems() -> [Either] { let items: [Either] switch settings.searchSuggestionsMode { case .tags: items = manager.allTags .map { Either.left($0) } case .history: items = settings.searchHistory .map { Either.right($0) } case .disabled: items = [] } return items } private func updateViewState( for queryID: UUID, posts: [BooruPost] = [], currentPage: Int? = nil ) { var state = viewStates[queryID] ?? PostGridViewState() if !posts.isEmpty { state.posts = posts } if let currentPage { state.currentPage = currentPage } viewStates[queryID] = state } }