diff options
| author | Fuwn <[email protected]> | 2025-07-10 08:07:51 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-07-10 08:07:51 -0700 |
| commit | 8c9d4a94e06d6c7411835acc22b69bb50b5448e6 (patch) | |
| tree | 85f59ff83fddb207626e9ee944a0fe104acb85cb /Sora/Views/Post | |
| parent | feat: Development commit (diff) | |
| download | sora-testing-8c9d4a94e06d6c7411835acc22b69bb50b5448e6.tar.xz sora-testing-8c9d4a94e06d6c7411835acc22b69bb50b5448e6.zip | |
feat: Development commit
Diffstat (limited to 'Sora/Views/Post')
| -rw-r--r-- | Sora/Views/Post/Grid/PostGridView.swift | 543 |
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 { |