summaryrefslogtreecommitdiff
path: root/Sora/Views/Post/Grid/PostGridView.swift
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-08-28 15:14:10 -0700
committerFuwn <[email protected]>2025-08-28 15:14:10 -0700
commita741e13d18a0927e984dfb6eb3d760f49afe3896 (patch)
tree34ba76fddeb26c80d902e7c1790a7fd192a52ff3 /Sora/Views/Post/Grid/PostGridView.swift
parentfeat: Development commit (diff)
downloadsora-testing-a741e13d18a0927e984dfb6eb3d760f49afe3896.tar.xz
sora-testing-a741e13d18a0927e984dfb6eb3d760f49afe3896.zip
feat: Development commit
Diffstat (limited to 'Sora/Views/Post/Grid/PostGridView.swift')
-rw-r--r--Sora/Views/Post/Grid/PostGridView.swift368
1 files changed, 221 insertions, 147 deletions
diff --git a/Sora/Views/Post/Grid/PostGridView.swift b/Sora/Views/Post/Grid/PostGridView.swift
index 2f2ec6d..ebeac31 100644
--- a/Sora/Views/Post/Grid/PostGridView.swift
+++ b/Sora/Views/Post/Grid/PostGridView.swift
@@ -8,33 +8,58 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length
@EnvironmentObject var manager: BooruManager
@State private var isSearchHistoryPresented = false
@Binding var selectedTab: Int
- @Binding var viewStates: [UUID: PostGridViewState]
- @Binding var viewStateSelection: UUID?
@State private var isSearchablePresented = false
@State private var cachedSuggestions: [Either<BooruTag, BooruSearchQuery>] = []
@State private var suppressNextSearchSubmit = false
@State private var searchTask: Task<Void, Never>?
@State private var suggestions: [BooruTag] = []
@State private var cachedColumnsData: ColumnsDataCache?
+ let initialTag: String?
+ @Binding var navigationPath: NavigationPath
+ @State private var localPosts: [BooruPost] = []
+ @State private var localIsLoading = false
+ @State private var localCurrentPage = 1
+ @State private var localSearchText = ""
+ @State private var localEndOfData = false
+ @State private var localError: Error?
+
+ init(
+ selectedTab: Binding<Int>, initialTag: String? = nil, navigationPath: Binding<NavigationPath>
+ ) {
+ self._selectedTab = selectedTab
+ self.initialTag = initialTag
+ self._navigationPath = navigationPath
+ }
@Environment(\.isSearching)
private var isSearching
- private var queryID: UUID {
- manager.searchHistory[manager.historyIndex].id
+ private var activePosts: [BooruPost] {
+ let posts = initialTag != nil ? localPosts : manager.posts
+
+ return posts.filter { settings.displayRatings.contains($0.rating) }
}
- private var activePosts: [BooruPost] {
- guard manager.historyIndex >= 0 && manager.historyIndex < manager.searchHistory.count else {
- return []
- }
+ private var isLoading: Bool {
+ initialTag != nil ? localIsLoading : manager.isLoading
+ }
- return viewStates[queryID]?.posts
- .filter { settings.displayRatings.contains($0.rating) } ?? []
+ private var searchText: Binding<String> {
+ if initialTag != nil {
+ return Binding(
+ get: { localSearchText },
+ set: { localSearchText = $0 }
+ )
+ } else {
+ return Binding(
+ get: { manager.searchText },
+ set: { manager.searchText = $0 }
+ )
+ }
}
@ViewBuilder private var gridContent: some View {
- if let error = manager.error {
+ if let error = (initialTag != nil ? localError : manager.error) {
ContentUnavailableView(
"Provider Error",
systemImage: "exclamationmark.triangle.fill",
@@ -42,7 +67,7 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length
)
}
- if activePosts.isEmpty, manager.isLoading {
+ if activePosts.isEmpty, isLoading {
placeholderGrid
} else {
gridView(columnCount: settings.thumbnailGridColumns)
@@ -132,19 +157,18 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length
var body: some View {
ScrollView {
gridContent
- .id(queryID)
.transition(.opacity)
}
#if os(iOS)
.searchable(
- text: $manager.searchText,
+ text: searchText,
isPresented: $isSearchablePresented,
placement: .navigationBarDrawer(displayMode: .automatic),
prompt: "Tags"
)
#else
.searchable(
- text: $manager.searchText,
+ text: searchText,
isPresented: $isSearchablePresented,
prompt: "Tags"
)
@@ -153,12 +177,12 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length
if settings.searchSuggestionsMode != .disabled && isSearchablePresented {
SearchSuggestionsView(
items: searchSuggestionsItems(),
- searchText: $manager.searchText,
+ searchText: searchText,
suppressNextSearchSubmit: $suppressNextSearchSubmit
)
}
}
- .onChange(of: manager.searchText) { _, newValue in
+ .onChange(of: searchText.wrappedValue) { _, newValue in
if settings.searchSuggestionsMode == .tags {
searchTask?.cancel()
@@ -180,58 +204,37 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length
.onSubmit(of: .search) {
if suppressNextSearchSubmit {
suppressNextSearchSubmit = false
-
return
}
Task(priority: .userInitiated) {
- await manager.performSearch(settings: settings)
+ if initialTag != nil {
+ await performLocalSearch()
+ } else {
+ await manager.performSearch(settings: settings)
+ }
}
}
- .navigationDestination(for: BooruPost.self) { post in
- PostDetailsView(post: post)
+ .navigationDestination(for: PostWithContext.self) { context in
+ PostDetailsView(post: context.post, navigationPath: $navigationPath, posts: context.posts)
}
.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)
- }
- }
- .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)
+ if !isPresented, searchText.wrappedValue.isEmpty, !manager.isNavigatingHistory {
+ Task(priority: .userInitiated) {
+ if initialTag != nil {
+ await performLocalSearch()
+ } else {
+ await manager.performSearch()
+ }
+ }
}
}
- .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 }
-
- 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
+ .onAppear {
+ if let initialTag = initialTag {
+ localSearchText = initialTag
Task(priority: .userInitiated) {
- await manager.fetchPosts(
- page: 1,
- tags: manager.searchHistory[newIndex].tags,
- replace: true
- )
+ await performLocalSearch()
}
}
}
@@ -240,12 +243,20 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length
ToolbarItem {
Button(action: {
Task {
- await manager.fetchPosts(page: 1, tags: manager.tags, replace: true)
+ if initialTag != nil {
+ await fetchLocalPosts(
+ page: 1,
+ tags: localSearchText.components(separatedBy: .whitespaces).filter {
+ !$0.isEmpty
+ }, replace: true)
+ } else {
+ await manager.fetchPosts(page: 1, tags: manager.tags, replace: true)
+ }
}
}) {
Label("Refresh", systemImage: "arrow.clockwise")
}
- .disabled(manager.isLoading)
+ .disabled(isLoading)
}
#endif
@@ -256,21 +267,30 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length
}
}
- if #available(iOS 26, *), manager.isLoading || manager.isNavigatingHistory {
+ if #available(iOS 26, *), isLoading || manager.isNavigatingHistory {
ToolbarItem(placement: .status) { ProgressView() }
}
#endif
PlatformSpecificToolbarItem {
- PostGridBookmarkButtonView()
- .disabled(manager.tags.isEmpty)
+ PostGridBookmarkButtonView(
+ tags: initialTag != nil
+ ? localSearchText.components(separatedBy: .whitespaces).filter { !$0.isEmpty }
+ : manager.tags,
+ provider: manager.provider
+ )
+ .disabled(searchText.wrappedValue.isEmpty)
}
PlatformSpecificToolbarItem {
Button(
action: {
Task(priority: .userInitiated) {
- await manager.loadNextPage()
+ if initialTag != nil {
+ await loadLocalNextPage()
+ } else {
+ await manager.loadNextPage()
+ }
}
}
) {
@@ -279,67 +299,75 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length
systemImage: "arrow.down.to.line"
)
}
- .disabled(manager.isLoading)
+ .disabled(isLoading)
}
#if !os(macOS)
- if #unavailable(iOS 26), manager.isLoading || manager.isNavigatingHistory {
+ if #unavailable(iOS 26), 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: " "))
+ if initialTag == nil {
+ 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()
}
}
- } label: {
- Label("Previous Search", systemImage: "chevron.left")
- } primaryAction: {
- withAnimation {
- manager.goBackInHistory()
- }
+ .disabled(!manager.canGoBackInHistory)
+ .id("previousSearchMenu")
}
- .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: " "))
+ 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()
}
}
- } label: {
- Label("Next Search", systemImage: "chevron.right")
- } primaryAction: {
- withAnimation {
- manager.goForwardInHistory()
- }
+ .disabled(!manager.canGoForwardInHistory)
+ .id("nextSearchMenu")
}
- .disabled(!manager.canGoForwardInHistory)
- .id("nextSearchMenu")
}
}
- .navigationTitle("Posts")
+ .navigationTitle(initialTag != nil ? initialTag! : "Posts")
.refreshable {
- manager.clearCachedPages()
-
- Task(priority: .userInitiated) {
- await manager.fetchPosts(page: 1, tags: manager.tags, replace: true)
+ if initialTag != nil {
+ await fetchLocalPosts(
+ page: 1,
+ tags: localSearchText.components(separatedBy: .whitespaces).filter { !$0.isEmpty },
+ replace: true)
+ } else {
+ manager.clearCachedPages()
+ Task(priority: .userInitiated) {
+ await manager.fetchPosts(page: 1, tags: manager.tags, replace: true)
+ }
}
}
.sheet(isPresented: $isSearchHistoryPresented) {
@@ -352,22 +380,24 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length
.gesture(
DragGesture()
.onEnded { value in
- if value.startLocation.x < 50 && value.translation.width > 100 {
- withAnimation {
- manager.goBackInHistory()
+ if initialTag == nil {
+ if value.startLocation.x < 50 && value.translation.width > 100 {
+ withAnimation {
+ manager.goBackInHistory()
+ }
+
+ debugPrint("ContentView: Swipe left, \(manager.searchHistory)")
}
- debugPrint("ContentView: Swipe left, \(manager.searchHistory)")
- }
+ if value.startLocation.x > (UIScreen.main.bounds.width - 50)
+ && value.translation.width < -100
+ {
+ withAnimation {
+ manager.goForwardInHistory()
+ }
- if value.startLocation.x > (UIScreen.main.bounds.width - 50)
- && value.translation.width < -100
- {
- withAnimation {
- manager.goForwardInHistory()
+ debugPrint("ContentView: Swipe right, \(manager.searchHistory)")
}
-
- debugPrint("ContentView: Swipe right, \(manager.searchHistory)")
}
}
)
@@ -375,17 +405,24 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length
}
private func waterfallGridContent(post: BooruPost) -> some View {
- Button {
- if !manager.isLoading { manager.selectedPost = post }
- } label: {
- PostGridThumbnailView(post: post, posts: activePosts)
+ NavigationLink(value: PostWithContext(post: post, posts: initialTag != nil ? localPosts : nil))
+ {
+ PostGridThumbnailView(
+ post: post,
+ posts: activePosts,
+ isNestedView: initialTag != nil,
+ endOfData: initialTag != nil ? localEndOfData : manager.endOfData,
+ onLoadNextPage: {
+ if initialTag != nil {
+ await loadLocalNextPage()
+ } else {
+ await manager.loadNextPage()
+ }
+ },
+ selectedPost: initialTag != nil ? nil : manager.selectedPost
+ )
}
.buttonStyle(PlainButtonStyle())
- .contextMenu {
- Button(action: { manager.selectedPost = post }) {
- Label("Select Post", systemImage: "arrow.right.circle")
- }
- }
}
private func searchSuggestionsItems() -> [Either<BooruTag, BooruSearchQuery>] {
@@ -401,30 +438,67 @@ struct PostGridView: View { // swiftlint:disable:this type_body_length
}
}
- private func updateViewState(
- for queryID: UUID,
- posts: [BooruPost] = [],
- currentPage: Int? = nil,
- selectedPost: BooruPost? = nil,
- resetSelectedPost: Bool = false,
- ) {
- let wasNewlyCreated = viewStates[queryID] == nil
- var state = viewStates[queryID] ?? PostGridViewState()
+ // MARK: - Local Search Methods
+ private func performLocalSearch() async {
+ let inputTags = localSearchText.components(separatedBy: .whitespaces).filter { !$0.isEmpty }
- if !posts.isEmpty { state.posts = posts }
+ await fetchLocalPosts(page: 1, tags: inputTags, replace: true)
+ }
- if let currentPage { state.currentPage = currentPage }
+ private func loadLocalNextPage() async {
+ guard !localIsLoading else { return }
- if let selectedPost {
- state.selectedPost = selectedPost
- } else if resetSelectedPost {
- state.selectedPost = nil
- }
+ localCurrentPage += 1
+
+ let inputTags = localSearchText.components(separatedBy: .whitespaces).filter { !$0.isEmpty }
+
+ await fetchLocalPosts(page: localCurrentPage, tags: inputTags, replace: false)
+ }
+
+ private func fetchLocalPosts(
+ page: Int = 1, limit: Int = 100, tags: [String] = [], replace: Bool = false
+ ) async {
+ guard !localIsLoading else { return }
+
+ localIsLoading = true
+
+ defer { localIsLoading = false }
+
+ let flavor = manager.flavor
+ let provider = manager.provider
+ let pageValue = flavor == .gelbooru ? page - 1 : page
- viewStates[queryID] = state
+ guard let url = manager.urlForPosts(page: pageValue, limit: limit, tags: tags) else { return }
+
+ do {
+ let data = try await manager.requestURL(url)
+ let newPosts = await withCheckedContinuation { continuation in
+ DispatchQueue.global(qos: .userInitiated).async {
+ let parsedPosts = BooruManager.parsePosts(
+ from: data,
+ flavor: flavor,
+ provider: provider
+ )
+ .sorted { $0.createdAt > $1.createdAt }
+
+ continuation.resume(returning: parsedPosts)
+ }
+ }
+
+ withAnimation(nil) {
+ if replace {
+ localPosts = newPosts
+ localCurrentPage = 1
+ } else {
+ localPosts.append(contentsOf: newPosts)
+ }
+
+ localEndOfData = newPosts.isEmpty
+ }
+ } catch {
+ localError = error
- if wasNewlyCreated {
- viewStateSelection = queryID
+ debugPrint("PostGridView.fetchLocalPosts: \(error)")
}
}
}