diff --git a/Graboo/ContentView.swift b/Graboo/ContentView.swift index 9f5aa7d..3443636 100644 --- a/Graboo/ContentView.swift +++ b/Graboo/ContentView.swift @@ -7,23 +7,11 @@ import SwiftUI -final class ModelData: ObservableObject { +struct ContentView: View { - @Published private(set) var imgs: [GelbooruImage] = [] - var searchTerms: [String] - - init(searchTerms: [String]) { - self.searchTerms = searchTerms - GelbooruClient().searchTagImages(tags: searchTerms) { data, error in - self.imgs = error != nil ? [] : data! - } - } - -} - -struct ContentView: View { - - @EnvironmentObject var model: ModelData + var booru: C + @State var searchTerm: String + @State private var pics: [C.T] = [] let cols = [ GridItem(.flexible()), @@ -31,26 +19,37 @@ struct ContentView: View { GridItem(.flexible()), ] + func reloadSearchResults(_ tags: String) { + self.booru.searchTagImages(tags: tags) { (data: [C.T]?, error) in + self.pics = error != nil ? [] : data! + } + } + + var body: some View { NavigationView { ScrollView { - Text(model.searchTerms.joined(separator: ", ")) + Text(self.searchTerm) .font(.headline) - LazyVGrid(columns: cols, spacing: 10) { - ForEach(model.imgs, id: \.self) { pic in - VStack { - AsyncImage(url: URL(string: pic.sampleUrl)) { image in - image.resizable().aspectRatio(contentMode: .fit) - } placeholder: { - ProgressView() - } - .frame(width: 100, height: 100 * CGFloat(pic.sampleHeight)/CGFloat(pic.sampleWidth)) - Text(String(pic.id)) + LazyVGrid(columns: cols, spacing: 2) { + ForEach(self.pics, id: \.self) { pic in + AsyncImage(url: URL(string: pic.displayUrl())) { image in + image.resizable().aspectRatio(contentMode: .fit) + } placeholder: { + ProgressView() } + .frame(width: 100, height: 150) } } } - .navigationTitle("Graboo") + } + .navigationTitle("Graboo") + .searchable( + text: self.$searchTerm, + placement: .sidebar + ) + .onSubmit(of: .search) { + reloadSearchResults(self.searchTerm) } } } @@ -58,8 +57,7 @@ struct ContentView: View { struct ContentView_Previews: PreviewProvider { static var previews: some View { - ContentView() - .environmentObject(ModelData(searchTerms: ["hatsune_miku", "rating:general"])) + ContentView(booru: SafebooruClient(), searchTerm: "doki_doki_literature_club") } } diff --git a/Graboo/GrabooApp.swift b/Graboo/GrabooApp.swift index 86919f9..668f138 100644 --- a/Graboo/GrabooApp.swift +++ b/Graboo/GrabooApp.swift @@ -11,8 +11,7 @@ import SwiftUI struct GrabooApp: App { var body: some Scene { WindowGroup { - ContentView() - .environmentObject(ModelData(searchTerms: ["hatsune_miku", "rating:general"])) + ContentView(booru: SafebooruClient(), searchTerm: "doki_doki_literature_club") } } } diff --git a/Graboo/lib/Client.swift b/Graboo/lib/Client.swift index 0be58e5..b98949d 100644 --- a/Graboo/lib/Client.swift +++ b/Graboo/lib/Client.swift @@ -6,12 +6,30 @@ // import Foundation +import CoreGraphics -struct GelbooruSearchResults: Decodable { + +protocol BooruClient { + associatedtype T: BooruImage + + var baseUrl: String { get } + func searchTagImages(tags: String, completionHandler: @escaping ([T]?, Error?) -> Void) +} + +protocol BooruSearchResults: Decodable { + +} + +protocol BooruImage: Decodable, Hashable { + func aspectRatio() -> CGFloat + func displayUrl() -> String +} + +struct GelbooruSearchResults: BooruSearchResults { let post: [GelbooruImage]; } -struct GelbooruImage: Decodable, Hashable { +struct GelbooruImage: BooruImage { let id: Int; let fileUrl: String; let width: Int; @@ -24,19 +42,59 @@ struct GelbooruImage: Decodable, Hashable { let sampleHeight: Int; let tags: String; let rating: String; -} - -class GelbooruClient { - let baseUrl: String - init(baseUrl: String = "https://gelbooru.com") { - self.baseUrl = baseUrl + + func aspectRatio() -> CGFloat { + return CGFloat(self.width) / CGFloat(self.height); } - func searchTagImages(tags: [String], completionHandler: @escaping ([GelbooruImage]?, Error?) -> Void) { - let joinedTags = tags.joined(separator: " ").addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! - let url = URL(string: String(format: "%@/?page=dapi&s=post&q=index&json=1&tags=%@", self.baseUrl, joinedTags))! - let task = URLSession.shared.dataTask(with: url) { (data, _, error) in + func displayUrl() -> String { + if sampleWidth > 0 && sampleHeight > 0 { + return sampleUrl; + } else { + return fileUrl; + } + } +} + +func httpGetJson(url: String, completionHandler: @escaping (T?, Error?) -> Void) { + let task = URLSession.shared.dataTask(with: URL(string: url)!) { (data, _, error) in + guard let data = data, error == nil else { + DispatchQueue.main.async { + completionHandler(nil, error) + } + return + } + + let parser = JSONDecoder() + parser.keyDecodingStrategy = .convertFromSnakeCase + var result: T + do { + result = try parser.decode(T.self, from: data) + } catch let e { + DispatchQueue.main.async { + completionHandler(nil, e) + } + return + } + + DispatchQueue.main.async { + completionHandler(result, nil) + } + } + task.resume() +} + +class GelbooruClient: BooruClient { + private(set) var baseUrl: String + + init() { + self.baseUrl = "https://gelbooru.com" + } + + func searchTagImages(tags: String, completionHandler: @escaping ([GelbooruImage]?, Error?) -> Void) { + let joinedTags = tags.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! + httpGetJson(url: String(format: "%@/?page=dapi&s=post&q=index&json=1&tags=%@", self.baseUrl, joinedTags)) { (data: GelbooruSearchResults?, error) in guard let data = data, error == nil else { DispatchQueue.main.async { completionHandler(nil, error) @@ -44,22 +102,45 @@ class GelbooruClient { return } - let parser = JSONDecoder() - parser.keyDecodingStrategy = .convertFromSnakeCase - var searchResults: GelbooruSearchResults - do { - searchResults = try parser.decode(GelbooruSearchResults.self, from: data) - } catch let e { - DispatchQueue.main.async { - completionHandler(nil, e) - } - return - } - DispatchQueue.main.async { - completionHandler(searchResults.post, nil) + completionHandler(data.post, nil) } } - task.resume() + } +} + + +struct SafebooruImage: BooruImage { + let id: Int; + let image: String; + let width: Int; + let height: Int; + let sample: Bool; + let sampleWidth: Int; + let sampleHeight: Int; + let tags: String; + let rating: String; + let directory: String; + + + func aspectRatio() -> CGFloat { + return CGFloat(self.width) / CGFloat(self.height); + } + + func displayUrl() -> String { + return String(format: "https://safebooru.org/images/%@/%@", self.directory, self.image); + } +} + +class SafebooruClient: BooruClient { + private(set) var baseUrl: String + + init() { + self.baseUrl = "https://safebooru.org" + } + + func searchTagImages(tags: String, completionHandler: @escaping ([SafebooruImage]?, Error?) -> Void) { + let joinedTags = tags.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! + httpGetJson(url: String(format: "%@/?page=dapi&s=post&q=index&json=1&tags=%@", self.baseUrl, joinedTags), completionHandler: completionHandler) } }