summaryrefslogtreecommitdiff
path: root/Sora
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
parentfeat: Development commit (diff)
downloadsora-testing-a741e13d18a0927e984dfb6eb3d760f49afe3896.tar.xz
sora-testing-a741e13d18a0927e984dfb6eb3d760f49afe3896.zip
feat: Development commit
Diffstat (limited to 'Sora')
-rw-r--r--Sora/Data/Booru/BooruManager.swift8
-rw-r--r--Sora/Data/PostGridViewState/PostGridViewState.swift8
-rw-r--r--Sora/Data/PostGridViewState/PostGridViewStateItem.swift6
-rw-r--r--Sora/Data/PostWithContext.swift19
-rw-r--r--Sora/Views/ContentView.swift82
-rw-r--r--Sora/Views/Post/Details/PostDetailsTagsView.swift33
-rw-r--r--Sora/Views/Post/Details/PostDetailsView.swift43
-rw-r--r--Sora/Views/Post/Grid/PostGridBookmarkButtonView.swift8
-rw-r--r--Sora/Views/Post/Grid/PostGridThumbnailView.swift11
-rw-r--r--Sora/Views/Post/Grid/PostGridView.swift368
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)")
}
}
}