summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-07-08 09:47:39 -0700
committerFuwn <[email protected]>2025-07-08 09:47:39 -0700
commita43bdd31d3e053de100773dfaa34e7225dcf49a8 (patch)
treeb52a3a3636f76f2eb7c6e57c288f617d15e2b543
parentfeat: Development commit (diff)
downloadsora-testing-a43bdd31d3e053de100773dfaa34e7225dcf49a8.tar.xz
sora-testing-a43bdd31d3e053de100773dfaa34e7225dcf49a8.zip
feat: Development commit
-rw-r--r--Sora/Data/PostGridViewState/PostGridViewState.swift1
-rw-r--r--Sora/Data/Scroll/ScrollPositionPreference.swift6
-rw-r--r--Sora/Data/Scroll/ScrollPositionPreferenceKey.swift19
-rw-r--r--Sora/Views/Post/Grid/PostGridView.swift197
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 {