import SwiftUI struct ContentView: View { @State private var posts: [MoebooruPost] = [] #if os(macOS) @State private var selectedPost: YanderePost? #endif @State private var searchQuery = "" @State private var currentPage = 1 @State private var isLoading: Bool = false @State private var largerThumbnails: Bool = false var tags: [String] { if searchQuery.isEmpty { return [] } return searchQuery .split(separator: ",") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } } @State var softLimit: Int = 100 var softLimitCGFloat: CGFloat { CGFloat(softLimit) } var columns: [GridItem] { return [ GridItem(.adaptive(minimum: softLimitCGFloat)), ] } var body: some View { #if os(macOS) NavigationSplitView { ScrollView { if self.posts.isEmpty { ProgressView() .frame(width: softLimitCGFloat, height: softLimitCGFloat) } LazyVGrid(columns: columns) { ForEach(posts, id: \.id) { post in Button { selectedPost = post } label: { PostView(post: post, softLimit: softLimitCGFloat) .frame(maxWidth: .infinity) } .buttonStyle(PlainButtonStyle()) if post == posts.last { ProgressView() .onAppear(perform: loadNextPage) .padding() } } } } .searchable(text: $searchQuery, prompt: "Tags") .onSubmit(of: .search, performSearch) .toolbar { ToolbarItem { Button(action: { Task { await fetchPosts(page: 1, tags: tags, replace: true) } }) { Label("Refresh", systemImage: "arrow.clockwise") } } } } detail: { if let post = selectedPost { PostDetailView(post: post) } else { Text("Select a post") .foregroundColor(.secondary) } } .task { await fetchPosts(page: currentPage) } #else NavigationStack { ScrollView { if self.posts.isEmpty { ProgressView() .frame(width: softLimitCGFloat, height: softLimitCGFloat) } LazyVGrid(columns: columns, spacing: 10) { ForEach(posts, id: \.id) { post in NavigationLink(value: post) { PostView( post: post, softLimit: softLimitCGFloat, thumbnailMode: self.largerThumbnails ? .sample : .preview ) .frame(maxWidth: .infinity) } if post == posts.last { ProgressView() .onAppear(perform: loadNextPage) .padding() } } } } .searchable(text: $searchQuery, prompt: "Tags") .onSubmit(of: .search, performSearch) .navigationDestination(for: MoebooruPost.self) { post in PostDetailView(post: post) } .task { await fetchPosts(page: currentPage) } .toolbar { ToolbarItem { TextField( softLimit.description, value: $softLimit, format: .number ) } ToolbarItem { Toggle("Full size thumbnails", isOn: $largerThumbnails) } ToolbarItem { Button(action: { Task { await fetchPosts(page: 1, tags: tags, replace: true) } }) { Label("Refresh", systemImage: "arrow.clockwise") } } } } #endif } func fetchPosts(page: Int = 1, limit: Int = 100, tags: [String] = [], replace: Bool = false) async { guard !isLoading else { return } isLoading = true defer { isLoading = false } guard let url = URL(string: "https://yande.re/post.xml?page=\(page)&limit=\(limit)&tags=\(tags.joined(separator: "+"))") else { return } do { let (data, _) = try await URLSession.shared.data(from: url) DispatchQueue.main.async { if replace { self.posts = [] self.currentPage = 1 } self.posts += Array(Set(MoebooruXMLParser().parse(data: data))).sorted { $0.id > $1.id } } } catch { #if DEBUG print(error) #endif } } func performSearch() { Task { await fetchPosts( page: 1, tags: tags, replace: true ) } } func loadNextPage() { guard !isLoading else { return } Task { await fetchPosts(page: currentPage + 1, tags: tags) DispatchQueue.main.async { currentPage += 1 } } } } #Preview { ContentView() }