diff options
| author | Fuwn <[email protected]> | 2025-07-08 09:47:39 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-07-08 09:47:39 -0700 |
| commit | a43bdd31d3e053de100773dfaa34e7225dcf49a8 (patch) | |
| tree | b52a3a3636f76f2eb7c6e57c288f617d15e2b543 | |
| parent | feat: Development commit (diff) | |
| download | sora-testing-a43bdd31d3e053de100773dfaa34e7225dcf49a8.tar.xz sora-testing-a43bdd31d3e053de100773dfaa34e7225dcf49a8.zip | |
feat: Development commit
| -rw-r--r-- | Sora/Data/PostGridViewState/PostGridViewState.swift | 1 | ||||
| -rw-r--r-- | Sora/Data/Scroll/ScrollPositionPreference.swift | 6 | ||||
| -rw-r--r-- | Sora/Data/Scroll/ScrollPositionPreferenceKey.swift | 19 | ||||
| -rw-r--r-- | Sora/Views/Post/Grid/PostGridView.swift | 197 |
4 files changed, 145 insertions, 78 deletions
diff --git a/Sora/Data/PostGridViewState/PostGridViewState.swift b/Sora/Data/PostGridViewState/PostGridViewState.swift index 266d05c..ff7a32a 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 scrollPosition: BooruPost.ID? let createdAt = Date() } diff --git a/Sora/Data/Scroll/ScrollPositionPreference.swift b/Sora/Data/Scroll/ScrollPositionPreference.swift new file mode 100644 index 0000000..fb36bb1 --- /dev/null +++ b/Sora/Data/Scroll/ScrollPositionPreference.swift @@ -0,0 +1,6 @@ +import SwiftUI + +struct ScrollPositionPreference: Equatable { + let id: BooruPost.ID + let yPosition: CGFloat +} diff --git a/Sora/Data/Scroll/ScrollPositionPreferenceKey.swift b/Sora/Data/Scroll/ScrollPositionPreferenceKey.swift new file mode 100644 index 0000000..6b78bdc --- /dev/null +++ b/Sora/Data/Scroll/ScrollPositionPreferenceKey.swift @@ -0,0 +1,19 @@ +import SwiftUI + +struct ScrollPositionPreferenceKey: PreferenceKey { + typealias Value = ScrollPositionPreference? + + static var defaultValue: Value = nil + + static func reduce(value: inout Value, nextValue: () -> Value) { + guard let next = nextValue() else { return } + + if let current = value { + if abs(next.yPosition) < abs(current.yPosition) { + value = next + } + } else { + value = next + } + } +} diff --git a/Sora/Views/Post/Grid/PostGridView.swift b/Sora/Views/Post/Grid/PostGridView.swift index 97604e5..5b48e1e 100644 --- a/Sora/Views/Post/Grid/PostGridView.swift +++ b/Sora/Views/Post/Grid/PostGridView.swift @@ -1,3 +1,5 @@ +// swiftlint:disable file_length + import SwiftUI import WaterfallGrid @@ -13,100 +15,120 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length @State private var suppressNextSearchSubmit = false @State private var searchTask: Task<Void, Never>? @State private var suggestions: [BooruTag] = [] + @State private var topItemID: BooruPost.ID? + @State private var debounceTask: Task<Void, Never>? @Environment(\.isSearching) private var isSearching - var filteredPosts: [BooruPost] { - manager.posts - .filter { settings.displayRatings.contains($0.rating) } + private var activePosts: [BooruPost] { + guard manager.historyIndex >= 0 && manager.historyIndex < manager.searchHistory.count else { + return [] + } + + let queryID = manager.searchHistory[manager.historyIndex].id + + return viewStates[queryID]?.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 { - Group { - if let error = manager.error { - ContentUnavailableView( - "Provider Error", - systemImage: "exclamationmark.triangle.fill", - description: Text(error.localizedDescription) - ) - } + ScrollViewReader { proxy in + ScrollView { + Group { + if let error = manager.error { + ContentUnavailableView( + "Provider Error", + systemImage: "exclamationmark.triangle.fill", + description: Text(error.localizedDescription) + ) + } - if filteredPosts.isEmpty, isActive, manager.isLoading { - let gridItems = Array( - repeating: GridItem(.flexible()), - count: settings.thumbnailGridColumns - ) + if activePosts.isEmpty, manager.isLoading { + let gridItems = Array( + repeating: GridItem(.flexible()), + count: settings.thumbnailGridColumns + ) - LazyVGrid(columns: gridItems) { - ForEach(0..<(50 / settings.thumbnailGridColumns), id: \.self) { _ in - PostGridThumbnailPlaceholderView() - } + 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 + + if settings.alternativeThumbnailGrid { + let columnsData = (0..<columnCount).map { columnIndex in + activePosts.enumerated().compactMap { index, post in + index % columnCount == columnIndex ? post : nil } - #if os(macOS) - .padding(8) - #else - .padding(.horizontal) - #endif - .transition(.opacity) - } else { - let columnCount = settings.thumbnailGridColumns - - if settings.alternativeThumbnailGrid { - let columnsData = (0..<columnCount).map { columnIndex in - filteredPosts.enumerated().compactMap { index, post in - index % columnCount == columnIndex ? post : nil - } - } + } - HStack(alignment: .top) { - ForEach(0..<columnCount, id: \.self) { columnIndex in - LazyVStack { - ForEach(columnsData[columnIndex], id: \.id) { post in - waterfallGridContent(post: post) - .id(post.id) - } - } - .transaction { $0.animation = nil } + HStack(alignment: .top) { + ForEach(0..<columnCount, id: \.self) { columnIndex in + LazyVStack { + ForEach(columnsData[columnIndex], id: \.id) { post in + waterfallGridContent(post: post) + .id(post.id) } } - #if os(macOS) - .padding(8) - #else - .padding(.horizontal) - #endif - .transition(.opacity) - } else { - WaterfallGrid(filteredPosts, id: \.id) { post in - waterfallGridContent(post: post) - .id(post.id) - } - .gridStyle(columns: columnCount) .transaction { $0.animation = nil } - #if os(macOS) - .padding(8) - #else - .padding(.horizontal) - #endif - .transition(.opacity) } } + #if os(macOS) + .padding(8) + #else + .padding(.horizontal) + #endif + .transition(.opacity) + } else { + WaterfallGrid(activePosts, id: \.id) { post in + waterfallGridContent(post: post) + .id(post.id) + } + .gridStyle(columns: columnCount) + .transaction { $0.animation = nil } + #if os(macOS) + .padding(8) + #else + .padding(.horizontal) + #endif + .transition(.opacity) } - .animation(.easeInOut, value: manager.isLoading) } - .id(query.id) - .opacity(isActive ? 1 : 0) - .frame(height: isActive ? nil : 0) - .animation(.easeInOut, value: isActive) + } + .id(manager.historyIndex) + .animation(.easeInOut, value: manager.isLoading) + } + .coordinateSpace(name: "scrollview") + .onPreferenceChange(ScrollPositionPreferenceKey.self) { preference in + topItemID = preference?.id + } + .onChange(of: topItemID) { _, newID in + debounceTask?.cancel() + + debounceTask = Task { + do { + try await Task.sleep(for: .seconds(0.5)) + + guard let newID, + manager.historyIndex >= 0, + manager.historyIndex < manager.searchHistory.count + else { return } + + let queryID = manager.searchHistory[manager.historyIndex].id + + updateViewState(for: queryID, scrollPosition: newID) + } catch { + return + } } } .animation(.easeInOut, value: manager.historyIndex) @@ -199,6 +221,12 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length manager.posts = state.posts manager.currentPage = state.currentPage manager.selectedPost = state.selectedPost + + if let scrollPosition = state.scrollPosition { + DispatchQueue.main.async { + proxy.scrollTo(scrollPosition, anchor: .top) + } + } } else { manager.posts = [] manager.currentPage = 1 @@ -344,7 +372,17 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length Button { if !manager.isLoading { manager.selectedPost = post } } label: { - PostGridThumbnailView(post: post, posts: filteredPosts) + PostGridThumbnailView(post: post, posts: activePosts) + .background( + GeometryReader { geometry in + let frame = geometry.frame(in: .named("scrollview")) + + Color.clear.preference( + key: ScrollPositionPreferenceKey.self, + value: ScrollPositionPreference(id: post.id, yPosition: frame.minY) + ) + } + ) } .buttonStyle(PlainButtonStyle()) .contextMenu { @@ -372,7 +410,8 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length posts: [BooruPost] = [], currentPage: Int? = nil, selectedPost: BooruPost? = nil, - resetSelectedPost: Bool = false + resetSelectedPost: Bool = false, + scrollPosition: BooruPost.ID? = nil ) { let wasNewlyCreated = viewStates[queryID] == nil var state = viewStates[queryID] ?? PostGridViewState() @@ -381,6 +420,8 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length if let currentPage { state.currentPage = currentPage } + if let scrollPosition { state.scrollPosition = scrollPosition } + if let selectedPost { state.selectedPost = selectedPost } else if resetSelectedPost { |