diff options
| author | Fuwn <[email protected]> | 2025-08-28 15:14:10 -0700 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-08-28 15:14:10 -0700 |
| commit | a741e13d18a0927e984dfb6eb3d760f49afe3896 (patch) | |
| tree | 34ba76fddeb26c80d902e7c1790a7fd192a52ff3 /Sora | |
| parent | feat: Development commit (diff) | |
| download | sora-testing-a741e13d18a0927e984dfb6eb3d760f49afe3896.tar.xz sora-testing-a741e13d18a0927e984dfb6eb3d760f49afe3896.zip | |
feat: Development commit
Diffstat (limited to 'Sora')
| -rw-r--r-- | Sora/Data/Booru/BooruManager.swift | 8 | ||||
| -rw-r--r-- | Sora/Data/PostGridViewState/PostGridViewState.swift | 8 | ||||
| -rw-r--r-- | Sora/Data/PostGridViewState/PostGridViewStateItem.swift | 6 | ||||
| -rw-r--r-- | Sora/Data/PostWithContext.swift | 19 | ||||
| -rw-r--r-- | Sora/Views/ContentView.swift | 82 | ||||
| -rw-r--r-- | Sora/Views/Post/Details/PostDetailsTagsView.swift | 33 | ||||
| -rw-r--r-- | Sora/Views/Post/Details/PostDetailsView.swift | 43 | ||||
| -rw-r--r-- | Sora/Views/Post/Grid/PostGridBookmarkButtonView.swift | 8 | ||||
| -rw-r--r-- | Sora/Views/Post/Grid/PostGridThumbnailView.swift | 11 | ||||
| -rw-r--r-- | Sora/Views/Post/Grid/PostGridView.swift | 368 |
10 files changed, 330 insertions, 256 deletions
diff --git a/Sora/Data/Booru/BooruManager.swift b/Sora/Data/Booru/BooruManager.swift index 59bdf24..5160c9d 100644 --- a/Sora/Data/Booru/BooruManager.swift +++ b/Sora/Data/Booru/BooruManager.swift @@ -102,7 +102,7 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng let provider = self.provider let newPosts = await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { - let parsedPosts = self.parsePosts( + let parsedPosts = BooruManager.parsePosts( from: data, flavor: flavor, provider: provider @@ -238,7 +238,7 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng } // MARK: - Private Methods - private func urlForPosts(page: Int, limit: Int, tags: [String]) -> URL? { + func urlForPosts(page: Int, limit: Int, tags: [String]) -> URL? { let tagString = tags.joined(separator: "+") switch flavor { @@ -304,7 +304,7 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng } } - nonisolated private func parsePosts( + nonisolated static func parsePosts( from data: Data, flavor: BooruProviderFlavor, provider: BooruProvider @@ -345,7 +345,7 @@ class BooruManager: ObservableObject { // swiftlint:disable:this type_body_leng } } - private func requestURL(_ url: URL) async throws -> Data { + func requestURL(_ url: URL) async throws -> Data { try await AF.request(url, headers: ["User-Agent": userAgent]) .serializingData() .value diff --git a/Sora/Data/PostGridViewState/PostGridViewState.swift b/Sora/Data/PostGridViewState/PostGridViewState.swift deleted file mode 100644 index 266d05c..0000000 --- a/Sora/Data/PostGridViewState/PostGridViewState.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -struct PostGridViewState: Equatable { - var posts: [BooruPost] = [] - var currentPage: Int = 1 - var selectedPost: BooruPost? - let createdAt = Date() -} diff --git a/Sora/Data/PostGridViewState/PostGridViewStateItem.swift b/Sora/Data/PostGridViewState/PostGridViewStateItem.swift deleted file mode 100644 index 3eb66c9..0000000 --- a/Sora/Data/PostGridViewState/PostGridViewStateItem.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -struct PostGridViewStateItem: Equatable { - let id: UUID - let state: PostGridViewState -} diff --git a/Sora/Data/PostWithContext.swift b/Sora/Data/PostWithContext.swift new file mode 100644 index 0000000..c8cb2f6 --- /dev/null +++ b/Sora/Data/PostWithContext.swift @@ -0,0 +1,19 @@ +import Foundation + +struct PostWithContext: Hashable { + let post: BooruPost + let posts: [BooruPost]? + + init(post: BooruPost, posts: [BooruPost]?) { + self.post = post + self.posts = posts + } + + func hash(into hasher: inout Hasher) { + hasher.combine(post.id) + } + + static func == (lhs: PostWithContext, rhs: PostWithContext) -> Bool { + lhs.post.id == rhs.post.id + } +} diff --git a/Sora/Views/ContentView.swift b/Sora/Views/ContentView.swift index a88fa1b..8adb8f5 100644 --- a/Sora/Views/ContentView.swift +++ b/Sora/Views/ContentView.swift @@ -3,37 +3,15 @@ import SwiftUI struct ContentView: View { @EnvironmentObject var manager: BooruManager @Binding var selectedTab: Int - @State private var viewStates: [UUID: PostGridViewState] = [:] - @State private var viewStateSelection: UUID? @State private var columnVisibility = NavigationSplitViewVisibility.doubleColumn - - var sortedViewStates: [PostGridViewStateItem] { - viewStates - .map { PostGridViewStateItem(id: $0.key, state: $0.value) } - .sorted { $0.state.createdAt > $1.state.createdAt } - } - - var history: [UUID: BooruSearchQuery] { - Dictionary(uniqueKeysWithValues: manager.searchHistory.map { ($0.id, $0) }) - } + @State private var navigationPath = NavigationPath() var body: some View { #if os(macOS) NavigationSplitView(columnVisibility: $columnVisibility) { - List(selection: $viewStateSelection) { - if viewStates.isEmpty { - Text("No Tags") - .tag(UUID.nilUUID()) - } - - ForEach(sortedViewStates, id: \.id) { item in - if let entry = history[item.id] { - let tags = entry.tags.joined(separator: " ") - - Text(tags.isEmpty ? "No Tags" : tags) - .tag(item.id) - } - } + List { + Text("Posts") + .tag(0) } } content: { switch selectedTab { @@ -50,52 +28,30 @@ struct ContentView: View { SettingsView() default: - PostGridView( - selectedTab: $selectedTab, - viewStates: $viewStates, - viewStateSelection: $viewStateSelection - ) + PostGridView(selectedTab: $selectedTab, navigationPath: $navigationPath) } } detail: { if let post = manager.selectedPost { - PostDetailsView(post: post) + PostDetailsView(post: post, navigationPath: $navigationPath, posts: nil) } else { Text("Select a Post") .foregroundColor(.secondary) } } - .onChange(of: viewStateSelection) { _, newValue in - guard let selectedID = newValue else { return } - - if let index = manager.searchHistory.firstIndex(where: { $0.id == selectedID }) { - manager.historyIndex = index - } - } - .onChange(of: sortedViewStates) { _, newKeys in - if viewStateSelection == nil || !newKeys.contains(where: { $0.id == viewStateSelection }) { - viewStateSelection = newKeys.first?.id ?? UUID.nilUUID() - } - } - .onAppear { - if viewStates.isEmpty && viewStateSelection == nil { - viewStateSelection = UUID.nilUUID() - } - } #else - NavigationStack { - PostGridView( - selectedTab: $selectedTab, - viewStates: $viewStates, - viewStateSelection: $viewStateSelection - ) - .navigationDestination( - isPresented: Binding( - get: { manager.selectedPost != nil }, - set: { if !$0 { manager.selectedPost = nil } } - ) - ) { - if let post = manager.selectedPost { PostDetailsView(post: post) } - } + NavigationStack(path: $navigationPath) { + PostGridView(selectedTab: $selectedTab, navigationPath: $navigationPath) + .navigationDestination(for: BooruPost.self) { post in + PostDetailsView(post: post, navigationPath: $navigationPath, posts: nil) + } + .navigationDestination(for: PostWithContext.self) { context in + PostDetailsView( + post: context.post, navigationPath: $navigationPath, posts: context.posts) + } + .navigationDestination(for: String.self) { tag in + PostGridView( + selectedTab: $selectedTab, initialTag: tag, navigationPath: $navigationPath) + } } #endif } diff --git a/Sora/Views/Post/Details/PostDetailsTagsView.swift b/Sora/Views/Post/Details/PostDetailsTagsView.swift index bb69792..ca1dcae 100644 --- a/Sora/Views/Post/Details/PostDetailsTagsView.swift +++ b/Sora/Views/Post/Details/PostDetailsTagsView.swift @@ -4,16 +4,17 @@ struct PostDetailsTagsView: View { @EnvironmentObject var manager: BooruManager @EnvironmentObject var settings: SettingsManager @Binding var isPresented: Bool + @Binding var navigationPath: NavigationPath var tags: [String] var body: some View { List { ForEach(tags, id: \.self) { tag in Button(action: { - Task { @MainActor in - manager.searchText = tag + isPresented = false - search() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + navigateToTagInMainStack(tag) } }) { Text(tag) @@ -43,17 +44,41 @@ struct PostDetailsTagsView: View { .buttonStyle(.plain) #endif } + .navigationTitle("Tags") } private func search() { manager.selectedPost = nil isPresented = false } + + private func navigateToTagInMainStack(_ tag: String) { + navigationPath.append(tag) + + let query = BooruSearchQuery( + provider: settings.preferredBooru, + tags: [tag] + ) + + settings.appendToSearchHistory(query) + + if manager.searchHistory.last?.tags != [tag] { + if manager.historyIndex < manager.searchHistory.count - 1 { + manager.searchHistory = Array(manager.searchHistory[0...manager.historyIndex]) + } + + manager.searchHistory.append(query) + + manager.historyIndex = manager.searchHistory.count - 1 + } + } } #Preview { PostDetailsTagsView( - isPresented: .constant(true), tags: ["hololive", "absurdres", "nekomimi"] + isPresented: .constant(true), + navigationPath: .constant(NavigationPath()), + tags: ["hololive", "absurdres", "nekomimi"] ) .environmentObject(BooruManager(.danbooru)) .environmentObject(SettingsManager()) diff --git a/Sora/Views/Post/Details/PostDetailsView.swift b/Sora/Views/Post/Details/PostDetailsView.swift index 8d62f67..8051cf2 100644 --- a/Sora/Views/Post/Details/PostDetailsView.swift +++ b/Sora/Views/Post/Details/PostDetailsView.swift @@ -4,7 +4,11 @@ struct PostDetailsView: View { @EnvironmentObject var manager: BooruManager @EnvironmentObject var settings: SettingsManager let post: BooruPost + @Binding var navigationPath: NavigationPath @State private var loadingStage: BooruPostLoadingState = .loadingPreview + @State private var isTagsSheetPresented = false + let posts: [BooruPost]? + private var imageURL: URL? { switch settings.detailViewQuality { case .preview: @@ -17,11 +21,17 @@ struct PostDetailsView: View { post.fileURL } } - @State private var isTagsSheetPresented = false + + init(post: BooruPost, navigationPath: Binding<NavigationPath>, posts: [BooruPost]? = nil) { + self.post = post + self._navigationPath = navigationPath + self.posts = posts + } var filteredPosts: [BooruPost] { - manager.posts - .filter { settings.displayRatings.contains($0.rating) } + let sourcePosts = posts ?? manager.posts + + return sourcePosts.filter { settings.displayRatings.contains($0.rating) } } var body: some View { @@ -110,14 +120,7 @@ struct PostDetailsView: View { } #endif } - .sheet( - isPresented: $isTagsSheetPresented, - onDismiss: { - Task(priority: .userInitiated) { - await manager.performSearch(settings: settings) - } - } - ) { + .sheet(isPresented: $isTagsSheetPresented) { if #available(macOS 15.0, *) { tagsSheetContent() #if os(macOS) @@ -131,12 +134,16 @@ struct PostDetailsView: View { @ViewBuilder private func tagsSheetContent() -> some View { - PostDetailsTagsView(isPresented: $isTagsSheetPresented, tags: post.tags) - #if os(macOS) - .frame( - minHeight: (NSScreen.main?.frame.height ?? 1_080) / 2, - maxHeight: .infinity - ) - #endif + PostDetailsTagsView( + isPresented: $isTagsSheetPresented, + navigationPath: $navigationPath, + tags: post.tags + ) + #if os(macOS) + .frame( + minHeight: (NSScreen.main?.frame.height ?? 1_080) / 2, + maxHeight: .infinity + ) + #endif } } diff --git a/Sora/Views/Post/Grid/PostGridBookmarkButtonView.swift b/Sora/Views/Post/Grid/PostGridBookmarkButtonView.swift index 8f2effc..2dadfc3 100644 --- a/Sora/Views/Post/Grid/PostGridBookmarkButtonView.swift +++ b/Sora/Views/Post/Grid/PostGridBookmarkButtonView.swift @@ -3,18 +3,20 @@ import SwiftUI struct PostGridBookmarkButtonView: View { @EnvironmentObject private var manager: BooruManager @EnvironmentObject private var settings: SettingsManager + let tags: [String] + let provider: BooruProvider var contained: Bool { - let lowercaseTags = manager.tags.map { $0.lowercased() } + let lowercaseTags = tags.map { $0.lowercased() } return settings.bookmarks .contains { bookmark in bookmark.tags == lowercaseTags - && bookmark.provider == manager.provider + && bookmark.provider == provider } } var body: some View { - BookmarkMenuButtonView(tags: manager.tags, provider: manager.provider) + BookmarkMenuButtonView(tags: tags, provider: provider) } } diff --git a/Sora/Views/Post/Grid/PostGridThumbnailView.swift b/Sora/Views/Post/Grid/PostGridThumbnailView.swift index b415b3b..313704b 100644 --- a/Sora/Views/Post/Grid/PostGridThumbnailView.swift +++ b/Sora/Views/Post/Grid/PostGridThumbnailView.swift @@ -6,6 +6,11 @@ struct PostGridThumbnailView: View { @EnvironmentObject var manager: BooruManager let post: BooruPost let posts: [BooruPost] + let isNestedView: Bool + let endOfData: Bool + let onLoadNextPage: () async -> Void + let selectedPost: BooruPost? + private var thumbnailURL: URL? { switch settings.thumbnailQuality { case .preview: @@ -23,7 +28,7 @@ struct PostGridThumbnailView: View { private func primaryImageContent(image: Image) -> some View { let isFiltered = settings.blurRatings.contains(post.rating) - && manager.selectedPost?.id != post.id + && selectedPost?.id != post.id image .resizable() @@ -58,9 +63,9 @@ struct PostGridThumbnailView: View { imageContent(image: image) .onScrollVisibilityChange { visible in if posts.count > 4 && post == posts[posts.count - (posts.count / 4)], - !manager.endOfData, visible + !endOfData, visible { - Task(priority: .utility) { await manager.loadNextPage() } + Task(priority: .utility) { await onLoadNextPage() } } } } else { 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)") } } } |