summaryrefslogtreecommitdiff
path: root/Sora/Views/Post/Grid
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-07-10 08:07:51 -0700
committerFuwn <[email protected]>2025-07-10 08:07:51 -0700
commit8c9d4a94e06d6c7411835acc22b69bb50b5448e6 (patch)
tree85f59ff83fddb207626e9ee944a0fe104acb85cb /Sora/Views/Post/Grid
parentfeat: Development commit (diff)
downloadsora-testing-8c9d4a94e06d6c7411835acc22b69bb50b5448e6.tar.xz
sora-testing-8c9d4a94e06d6c7411835acc22b69bb50b5448e6.zip
feat: Development commit
Diffstat (limited to 'Sora/Views/Post/Grid')
-rw-r--r--Sora/Views/Post/Grid/PostGridView.swift543
1 files changed, 253 insertions, 290 deletions
diff --git a/Sora/Views/Post/Grid/PostGridView.swift b/Sora/Views/Post/Grid/PostGridView.swift
index 4f7fbd9..be56286 100644
--- a/Sora/Views/Post/Grid/PostGridView.swift
+++ b/Sora/Views/Post/Grid/PostGridView.swift
@@ -15,9 +15,6 @@ 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>?
- @State private var pendingScrollPosition: BooruPost.ID?
@Environment(\.isSearching)
private var isSearching
@@ -35,27 +32,67 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length
.filter { settings.displayRatings.contains($0.rating) } ?? []
}
+ private var scrollPosition: Binding<BooruPost.ID?> {
+ Binding(
+ get: {
+ viewStates[queryID]?.scrollPosition
+ },
+ set: { newPosition in
+ if viewStates[queryID] != nil {
+ viewStates[queryID]?.scrollPosition = newPosition
+ }
+ }
+ )
+ }
+
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)
- )
+ ScrollView {
+ Group {
+ 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
+ )
+
+ 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 activePosts.isEmpty, manager.isLoading {
- let gridItems = Array(
- repeating: GridItem(.flexible()),
- count: settings.thumbnailGridColumns
- )
+ if settings.alternativeThumbnailGrid {
+ let columnsData = (0..<columnCount).map { columnIndex in
+ activePosts.enumerated().compactMap { index, post in
+ index % columnCount == columnIndex ? post : nil
+ }
+ }
- LazyVGrid(columns: gridItems) {
- ForEach(0..<(50 / settings.thumbnailGridColumns), id: \.self) { _ in
- PostGridThumbnailPlaceholderView()
+ 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 }
+ .scrollTargetLayout()
}
}
#if os(macOS)
@@ -65,312 +102,251 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length
#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
- }
- }
-
- 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 }
- }
- }
- #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)
+ WaterfallGrid(activePosts, id: \.id) { post in
+ waterfallGridContent(post: post)
+ .id(post.id)
}
- }
- }
- .id(queryID)
- .animation(.easeInOut, value: manager.historyIndex)
- }
- .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
+ .gridStyle(columns: columnCount)
+ .transaction { $0.animation = nil }
+ .scrollTargetLayout()
+ #if os(macOS)
+ .padding(8)
+ #else
+ .padding(.horizontal)
+ #endif
+ .transition(.opacity)
}
}
}
- #if os(iOS)
- .searchable(
- text: $manager.searchText,
- isPresented: $isSearchablePresented,
- placement: .navigationBarDrawer(displayMode: .automatic),
- prompt: "Tags"
- )
- #else
- .searchable(
- text: $manager.searchText,
- isPresented: $isSearchablePresented,
- prompt: "Tags"
+ .id(queryID)
+ .animation(.easeInOut, value: manager.historyIndex)
+ }
+ .scrollPosition(id: scrollPosition)
+ #if os(iOS)
+ .searchable(
+ text: $manager.searchText,
+ isPresented: $isSearchablePresented,
+ placement: .navigationBarDrawer(displayMode: .automatic),
+ prompt: "Tags"
+ )
+ #else
+ .searchable(
+ text: $manager.searchText,
+ isPresented: $isSearchablePresented,
+ prompt: "Tags"
+ )
+ #endif
+ .searchSuggestions {
+ if settings.searchSuggestionsMode != .disabled {
+ SearchSuggestionsView(
+ items: searchSuggestionsItems(),
+ searchText: $manager.searchText,
+ suppressNextSearchSubmit: $suppressNextSearchSubmit
)
- #endif
- .searchSuggestions {
- if settings.searchSuggestionsMode != .disabled {
- SearchSuggestionsView(
- items: searchSuggestionsItems(),
- searchText: $manager.searchText,
- suppressNextSearchSubmit: $suppressNextSearchSubmit
- )
- }
}
- .onChange(of: manager.searchText) { _, newValue in
- if settings.searchSuggestionsMode == .tags {
- searchTask?.cancel()
+ }
+ .onChange(of: manager.searchText) { _, newValue in
+ if settings.searchSuggestionsMode == .tags {
+ searchTask?.cancel()
- searchTask = Task {
- try? await Task.sleep(nanoseconds: 300_000_000)
+ searchTask = Task {
+ try? await Task.sleep(nanoseconds: 300_000_000)
- guard !Task.isCancelled else { return }
+ guard !Task.isCancelled else { return }
- let searchTag = newValue.split(separator: " ").last.map(String.init) ?? ""
+ let searchTag = newValue.split(separator: " ").last.map(String.init) ?? ""
- if !searchTag.isEmpty {
- suggestions = await manager.searchTags(name: searchTag)
- } else {
- suggestions = []
- }
+ if !searchTag.isEmpty {
+ suggestions = await manager.searchTags(name: searchTag)
+ } else {
+ suggestions = []
}
}
}
- .onSubmit(of: .search) {
- if suppressNextSearchSubmit {
- suppressNextSearchSubmit = false
-
- return
- }
+ }
+ .onSubmit(of: .search) {
+ if suppressNextSearchSubmit {
+ suppressNextSearchSubmit = false
- Task(priority: .userInitiated) {
- await manager.performSearch(settings: settings)
- }
+ return
}
- .navigationDestination(for: BooruPost.self) { post in
- PostDetailsView(post: post)
+
+ Task(priority: .userInitiated) {
+ await manager.performSearch(settings: settings)
}
- .onChange(of: isSearchablePresented) { _, isPresented in
- if !isPresented, manager.searchText.isEmpty, !manager.isNavigatingHistory {
- Task(priority: .userInitiated) { await manager.performSearch() }
- }
+ }
+ .navigationDestination(for: BooruPost.self) { post in
+ PostDetailsView(post: post)
+ }
+ .onChange(of: isSearchablePresented) { _, isPresented in
+ if !isPresented, manager.searchText.isEmpty, !manager.isNavigatingHistory {
+ Task(priority: .userInitiated) { await manager.performSearch() }
}
- .onChange(of: manager.posts) { _, newPosts in
- if manager.historyIndex >= 0 && manager.historyIndex < manager.searchHistory.count {
- updateViewState(for: queryID, posts: newPosts)
-
- if let scrollID = pendingScrollPosition {
- proxy.scrollTo(scrollID, anchor: .top)
-
- pendingScrollPosition = nil
- } else {
- proxy.scrollTo(queryID, anchor: .top)
- }
- }
+ }
+ .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)
- }
- .onChange(of: manager.historyIndex) { _, newIndex in
- guard newIndex >= 0 && newIndex < manager.searchHistory.count else { return }
+ updateViewState(for: queryID, selectedPost: newPost, resetSelectedPost: newPost == nil)
+ }
+ .onChange(of: manager.historyIndex) { _, newIndex in
+ guard newIndex >= 0 && newIndex < manager.searchHistory.count else { return }
- let queryID = manager.searchHistory[newIndex].id
+ let queryID = manager.searchHistory[newIndex].id
- if let state = viewStates[queryID] {
- manager.posts = state.posts
- manager.currentPage = state.currentPage
- manager.selectedPost = state.selectedPost
- pendingScrollPosition = state.scrollPosition
- } else {
- manager.posts = []
- manager.currentPage = 1
-
- Task(priority: .userInitiated) {
- await manager.fetchPosts(
- page: 1,
- tags: manager.searchHistory[newIndex].tags,
- replace: true
- )
- }
+ if let state = viewStates[queryID] {
+ manager.posts = state.posts
+ manager.currentPage = state.currentPage
+ manager.selectedPost = state.selectedPost
+ } else {
+ manager.posts = []
+ manager.currentPage = 1
- pendingScrollPosition = nil
+ 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")
+ }
+ .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: {
- manager.goBackInHistory()
- }
- .disabled(!manager.canGoBackInHistory)
+ #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: " "))
- }
+ 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: {
- manager.goForwardInHistory()
}
- .disabled(!manager.canGoForwardInHistory)
+ } label: {
+ Label("Previous Search", systemImage: "chevron.left")
+ } primaryAction: {
+ manager.goBackInHistory()
}
+ .disabled(!manager.canGoBackInHistory)
}
- .navigationTitle("Posts")
- .refreshable {
- manager.clearCachedPages()
- Task(priority: .userInitiated) {
- await manager.fetchPosts(page: 1, tags: manager.tags, replace: true)
+ 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: {
+ manager.goForwardInHistory()
}
+ .disabled(!manager.canGoForwardInHistory)
}
- .sheet(isPresented: $isSearchHistoryPresented) {
- PostGridSearchHistoryView(
- selectedTab: $selectedTab,
- isPresented: $isSearchHistoryPresented
- )
+ }
+ .navigationTitle("Posts")
+ .refreshable {
+ manager.clearCachedPages()
+
+ Task(priority: .userInitiated) {
+ await manager.fetchPosts(page: 1, tags: manager.tags, replace: true)
}
- #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)")
- }
+ }
+ .sheet(isPresented: $isSearchHistoryPresented) {
+ PostGridSearchHistoryView(
+ selectedTab: $selectedTab,
+ isPresented: $isSearchHistoryPresented
+ )
+ }
+ #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)")
- }
+ if value.startLocation.x > (UIScreen.main.bounds.width - 50)
+ && value.translation.width < -100
+ {
+ manager.goForwardInHistory()
+ debugPrint("ContentView: Swipe right, \(manager.searchHistory)")
}
- )
- #endif
- }
+ }
+ )
+ #endif
}
private func waterfallGridContent(post: BooruPost) -> some View {
@@ -378,16 +354,6 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length
if !manager.isLoading { manager.selectedPost = post }
} label: {
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 {
@@ -416,7 +382,6 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length
currentPage: Int? = nil,
selectedPost: BooruPost? = nil,
resetSelectedPost: Bool = false,
- scrollPosition: BooruPost.ID? = nil
) {
let wasNewlyCreated = viewStates[queryID] == nil
var state = viewStates[queryID] ?? PostGridViewState()
@@ -425,8 +390,6 @@ 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 {