From f69eb3d8a7fb2d181f44376df53ff3765d12867e Mon Sep 17 00:00:00 2001 From: Fuwn Date: Thu, 24 Jul 2025 18:10:43 +0200 Subject: feat: Development commit --- .../Data/PostGridViewState/PostGridViewState.swift | 1 + Sora/Views/Post/Grid/PostGridView.swift | 574 +++++++++++---------- 2 files changed, 315 insertions(+), 260 deletions(-) diff --git a/Sora/Data/PostGridViewState/PostGridViewState.swift b/Sora/Data/PostGridViewState/PostGridViewState.swift index 266d05c..95e3396 100644 --- a/Sora/Data/PostGridViewState/PostGridViewState.swift +++ b/Sora/Data/PostGridViewState/PostGridViewState.swift @@ -4,5 +4,6 @@ struct PostGridViewState: Equatable { var posts: [BooruPost] = [] var currentPage: Int = 1 var selectedPost: BooruPost? + var scrollPostID: String? let createdAt = Date() } diff --git a/Sora/Views/Post/Grid/PostGridView.swift b/Sora/Views/Post/Grid/PostGridView.swift index f247370..99c7966 100644 --- a/Sora/Views/Post/Grid/PostGridView.swift +++ b/Sora/Views/Post/Grid/PostGridView.swift @@ -15,6 +15,8 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length @State private var suppressNextSearchSubmit = false @State private var searchTask: Task? @State private var suggestions: [BooruTag] = [] + private static let topID = "PostGridScrollTop" + @State private var scrollID: String? @Environment(\.isSearching) private var isSearching @@ -32,323 +34,366 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length .filter { settings.displayRatings.contains($0.rating) } ?? [] } - var body: some View { - ScrollViewReader { proxy in - ScrollView { - Group { - if let error = manager.error { - ContentUnavailableView( - "Provider Error", - systemImage: "exclamationmark.triangle.fill", - description: Text(error.localizedDescription) - ) - } + @ViewBuilder private var gridContent: some View { + if let error = manager.error { + ContentUnavailableView( + "Provider Error", + systemImage: "exclamationmark.triangle.fill", + description: Text(error.localizedDescription) + ) + } - if activePosts.isEmpty, manager.isLoading { - let gridItems = Array( - repeating: GridItem(.flexible()), - count: settings.thumbnailGridColumns - ) + if activePosts.isEmpty, manager.isLoading { + placeholderGrid + } else { + gridView(columnCount: settings.thumbnailGridColumns) + } + } - LazyVGrid(columns: gridItems) { - ForEach(0..<(50 / settings.thumbnailGridColumns), id: \.self) { _ in - PostGridThumbnailPlaceholderView() - } - } - #if os(macOS) - .padding(8) - #else - .padding(.horizontal) - #endif - .transition(.opacity) - } else { - let columnCount = settings.thumbnailGridColumns + @ViewBuilder private var placeholderGrid: some View { + let gridItems = Array( + repeating: GridItem(.flexible()), + count: settings.thumbnailGridColumns + ) - if settings.alternativeThumbnailGrid { - let columnsData = (0.. some View { + if settings.alternativeThumbnailGrid { + let columnsData = computeColumnsData(columnCount: columnCount) + + HStack(alignment: .top) { + ForEach(0.. [[BooruPost]] { + (0..= 0 && manager.historyIndex < manager.searchHistory.count { - updateViewState(for: queryID, posts: newPosts) - } + } + .onChange(of: manager.posts) { _, newPosts in + if manager.historyIndex >= 0 && manager.historyIndex < manager.searchHistory.count { + 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 + } + .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) - } + updateViewState(for: queryID, currentPage: newPage) } - .onChange(of: manager.selectedPost) { _, newPost in - let queryID = manager.searchHistory.last { $0.tags == manager.tags }?.id ?? UUID() + } + .onChange(of: manager.selectedPost) { _, newPost in + let queryID = manager.searchHistory.last { $0.tags == manager.tags }?.id ?? UUID() - updateViewState(for: queryID, selectedPost: newPost, resetSelectedPost: newPost == nil) + updateViewState(for: queryID, selectedPost: newPost, resetSelectedPost: newPost == nil) + } + .onChange(of: scrollID) { _, newValue in + guard manager.historyIndex >= 0 && manager.historyIndex < manager.searchHistory.count else { + return } - .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 - manager.selectedPost = state.selectedPost - } else { - manager.posts = [] - manager.currentPage = 1 - - Task(priority: .userInitiated) { - await manager.fetchPosts( - page: 1, - tags: manager.searchHistory[newIndex].tags, - replace: true - ) - } - } - proxy.scrollTo(queryID, anchor: .top) + let queryID = manager.searchHistory[manager.historyIndex].id + + updateViewState(for: queryID, scrollPostID: newValue) + } + .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 + manager.selectedPost = state.selectedPost + } else { + manager.posts = [] + manager.currentPage = 1 + + Task(priority: .userInitiated) { + await 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) - PlatformSpecificToolbarItem { - Button(action: { Task { isSearchHistoryPresented.toggle() } }) { - Label("Search History", systemImage: "clock.arrow.circlepath") + if let savedID = viewStates[queryID]?.scrollPostID { + scrollID = savedID + } else { + scrollID = Self.topID + } + } + .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 #available(iOS 26, *), manager.isLoading || manager.isNavigatingHistory { - ToolbarItem(placement: .status) { ProgressView() } + #if !os(macOS) + PlatformSpecificToolbarItem { + Button(action: { Task { isSearchHistoryPresented.toggle() } }) { + Label("Search History", systemImage: "clock.arrow.circlepath") } - #endif + } - PlatformSpecificToolbarItem { - PostGridBookmarkButtonView() - .disabled(manager.tags.isEmpty) + if #available(iOS 26, *), manager.isLoading || manager.isNavigatingHistory { + ToolbarItem(placement: .status) { ProgressView() } } + #endif - PlatformSpecificToolbarItem { - Button( - action: { - Task(priority: .userInitiated) { - await manager.loadNextPage() - } + PlatformSpecificToolbarItem { + PostGridBookmarkButtonView() + .disabled(manager.tags.isEmpty) + } + + PlatformSpecificToolbarItem { + Button( + action: { + Task(priority: .userInitiated) { + await manager.loadNextPage() } - ) { - Label( - "Manually Load Next Page", - systemImage: "arrow.down.to.line" - ) } - .disabled(manager.isLoading) + ) { + Label( + "Manually Load Next Page", + systemImage: "arrow.down.to.line" + ) } + .disabled(manager.isLoading) + } - #if !os(macOS) - if #unavailable(iOS 26), manager.isLoading || manager.isNavigatingHistory { - ToolbarItem(placement: .topBarTrailing) { ProgressView() } - } - #endif - - PlatformSpecificToolbarItem(placement: .navigation) { - Menu { - ForEach( - Array(manager.searchHistory.enumerated().filter { $0.offset < manager.historyIndex }), - id: \.offset - ) { offset, query in - Button(action: { - manager.historyIndex = offset - }) { - Text(query.tags.isEmpty ? "No Tags" : query.tags.joined(separator: " ")) - } - } - } label: { - Label("Previous Search", systemImage: "chevron.left") - } primaryAction: { - withAnimation { - manager.goBackInHistory() + #if !os(macOS) + if #unavailable(iOS 26), manager.isLoading || manager.isNavigatingHistory { + ToolbarItem(placement: .topBarTrailing) { ProgressView() } + } + #endif + + PlatformSpecificToolbarItem(placement: .navigation) { + Menu { + ForEach( + Array(manager.searchHistory.enumerated().filter { $0.offset < manager.historyIndex }), + id: \.offset + ) { offset, query in + Button(action: { + manager.historyIndex = offset + }) { + Text(query.tags.isEmpty ? "No Tags" : query.tags.joined(separator: " ")) } } - .disabled(!manager.canGoBackInHistory) - .id("previousSearchMenu") + } label: { + Label("Previous Search", systemImage: "chevron.left") + } primaryAction: { + withAnimation { + manager.goBackInHistory() + } } + .disabled(!manager.canGoBackInHistory) + .id("previousSearchMenu") + } - PlatformSpecificToolbarItem(placement: .navigation) { - Menu { - ForEach( - Array(manager.searchHistory.enumerated().filter { $0.offset > manager.historyIndex }), - id: \.offset - ) { offset, query in - Button(action: { - manager.historyIndex = offset - }) { - Text(query.tags.isEmpty ? "No Tags" : query.tags.joined(separator: " ")) - } - } - } label: { - Label("Next Search", systemImage: "chevron.right") - } primaryAction: { - withAnimation { - manager.goForwardInHistory() + PlatformSpecificToolbarItem(placement: .navigation) { + Menu { + ForEach( + Array(manager.searchHistory.enumerated().filter { $0.offset > manager.historyIndex }), + id: \.offset + ) { offset, query in + Button(action: { + manager.historyIndex = offset + }) { + Text(query.tags.isEmpty ? "No Tags" : query.tags.joined(separator: " ")) } } - .disabled(!manager.canGoForwardInHistory) - .id("nextSearchMenu") + } label: { + Label("Next Search", systemImage: "chevron.right") + } primaryAction: { + withAnimation { + manager.goForwardInHistory() + } } + .disabled(!manager.canGoForwardInHistory) + .id("nextSearchMenu") } - .navigationTitle("Posts") - .refreshable { - manager.clearCachedPages() + } + .navigationTitle("Posts") + .refreshable { + manager.clearCachedPages() - Task(priority: .userInitiated) { - await manager.fetchPosts(page: 1, tags: manager.tags, replace: true) - } + Task(priority: .userInitiated) { + await manager.fetchPosts(page: 1, tags: manager.tags, replace: true) } - .sheet(isPresented: $isSearchHistoryPresented) { - PostGridSearchHistoryView( - selectedTab: $selectedTab, - isPresented: $isSearchHistoryPresented - ) + } + .sheet(isPresented: $isSearchHistoryPresented) { + PostGridSearchHistoryView( + selectedTab: $selectedTab, + isPresented: $isSearchHistoryPresented + ) + } + .onAppear { + scrollID = viewStates[queryID]?.scrollPostID ?? Self.topID + } + .onDisappear { + guard manager.historyIndex >= 0 && manager.historyIndex < manager.searchHistory.count else { + return } - #if os(iOS) - .gesture( - DragGesture() - .onEnded { value in - if value.startLocation.x < 50 && value.translation.width > 100 { - withAnimation { - manager.goBackInHistory() - } - - debugPrint("ContentView: Swipe left, \(manager.searchHistory)") + + let queryID = manager.searchHistory[manager.historyIndex].id + + updateViewState(for: queryID, scrollPostID: scrollID) + } + #if os(iOS) + .gesture( + DragGesture() + .onEnded { value in + if value.startLocation.x < 50 && value.translation.width > 100 { + withAnimation { + manager.goBackInHistory() } - if value.startLocation.x > (UIScreen.main.bounds.width - 50) - && value.translation.width < -100 - { - withAnimation { - manager.goForwardInHistory() - } + debugPrint("ContentView: Swipe left, \(manager.searchHistory)") + } - debugPrint("ContentView: Swipe right, \(manager.searchHistory)") + if value.startLocation.x > (UIScreen.main.bounds.width - 50) + && value.translation.width < -100 + { + withAnimation { + manager.goForwardInHistory() } + + debugPrint("ContentView: Swipe right, \(manager.searchHistory)") } - ) - #endif - } + } + ) + #endif } private func waterfallGridContent(post: BooruPost) -> some View { @@ -383,6 +428,7 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length posts: [BooruPost] = [], currentPage: Int? = nil, selectedPost: BooruPost? = nil, + scrollPostID: String? = nil, resetSelectedPost: Bool = false, ) { let wasNewlyCreated = viewStates[queryID] == nil @@ -398,6 +444,14 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length state.selectedPost = nil } + if let scrollPostID { + state.scrollPostID = scrollPostID + } + + if wasNewlyCreated && state.scrollPostID == nil { + state.scrollPostID = Self.topID + } + viewStates[queryID] = state if wasNewlyCreated { -- cgit v1.2.3