diff options
| author | Fuwn <[email protected]> | 2025-02-22 00:07:44 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-02-22 00:07:44 -0800 |
| commit | e42fa12dafe264c665d2574c93b54ddafe7f2e1f (patch) | |
| tree | a362b3b78ea97dc28ce5cc3682801bf89688f546 | |
| parent | feat: Development commit (diff) | |
| download | sora-testing-e42fa12dafe264c665d2574c93b54ddafe7f2e1f.tar.xz sora-testing-e42fa12dafe264c665d2574c93b54ddafe7f2e1f.zip | |
feat: Development commit
27 files changed, 1048 insertions, 1010 deletions
diff --git a/Sora/Data/Booru/BooruManager.swift b/Sora/Data/Booru/BooruManager.swift index dd63367..f86e3a9 100644 --- a/Sora/Data/Booru/BooruManager.swift +++ b/Sora/Data/Booru/BooruManager.swift @@ -2,143 +2,151 @@ import SwiftUI @MainActor class BooruManager: ObservableObject { - @Published var posts: [BooruPost] = [] - @Published var allTags: [BooruTag] = [] - @Published var isLoading: Bool = false - @Published var currentPage: Int = 1 - @Published var searchText = "" - @Published var endOfData: Bool = false - #if os(macOS) - @Published var selectedPost: BooruPost? - #endif - private var currentTask: Task<Void, Never>? - let provider: BooruProvider? - var tags: [String] { - if searchText.isEmpty { - return [] - } - - return searchText - .split(separator: " ") - .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } - .filter { !$0.isEmpty } + @Published var posts: [BooruPost] = [] + @Published var allTags: [BooruTag] = [] + @Published var isLoading: Bool = false + @Published var currentPage: Int = 1 + @Published var searchText = "" + @Published var endOfData: Bool = false + #if os(macOS) + @Published var selectedPost: BooruPost? + #endif + private var currentTask: Task<Void, Never>? + let provider: BooruProvider? + var tags: [String] { + if searchText.isEmpty { + return [] } - init(_ provider: BooruProvider? = nil) { - self.provider = provider + return + searchText + .split(separator: " ") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty } + } - fetchAllTags() - } + init(_ provider: BooruProvider? = nil) { + self.provider = provider + + fetchAllTags() + } - func fetchPosts(page: Int = 1, limit: Int = 100, tags: [String] = [], replace: Bool = false) async { - guard !isLoading else { return } - guard provider != nil else { return } + func fetchPosts(page: Int = 1, limit: Int = 100, tags: [String] = [], replace: Bool = false) async + { + guard !isLoading else { return } + guard provider != nil else { return } - currentTask?.cancel() + currentTask?.cancel() - currentTask = Task { - isLoading = true + currentTask = Task { + isLoading = true - defer { isLoading = false } + defer { isLoading = false } - if replace { - self.posts = [] - self.currentPage = 1 - } + if replace { + self.posts = [] + self.currentPage = 1 + } - guard let url = urlForPosts(page: self.provider == .safebooru ? page - 1 : page, limit: limit, tags: tags) else { - return - } + guard + let url = urlForPosts( + page: self.provider == .safebooru ? page - 1 : page, limit: limit, tags: tags) + else { + return + } - do { - let (data, _) = try await URLSession.shared.data(from: url) + do { + let (data, _) = try await URLSession.shared.data(from: url) - if Task.isCancelled { return } + if Task.isCancelled { return } - DispatchQueue.main.async { - let newPosts = Array(Set(BooruPostXMLParser(data: data).parse())).sorted { $0.id > $1.id } + DispatchQueue.main.async { + let newPosts = Array(Set(BooruPostXMLParser(data: data).parse())).sorted { $0.id > $1.id } - if newPosts.isEmpty { - self.endOfData = true - } else { - self.posts += Array(Set(newPosts)) - } - } - } catch { - if (error as? URLError)?.code != .cancelled { - #if DEBUG - print("fetchPosts: \(error)") - #endif - } - } + if newPosts.isEmpty { + self.endOfData = true + } else { + self.posts += Array(Set(newPosts)) + } } + } catch { + if (error as? URLError)?.code != .cancelled { + #if DEBUG + print("fetchPosts: \(error)") + #endif + } + } } + } - func performSearch() { - currentTask?.cancel() + func performSearch() { + currentTask?.cancel() - currentTask = Task { - await fetchPosts(page: 1, tags: tags, replace: true) - } + currentTask = Task { + await fetchPosts(page: 1, tags: tags, replace: true) } + } - func loadNextPage() { - guard !isLoading else { return } + func loadNextPage() { + guard !isLoading else { return } - Task { - await fetchPosts(page: currentPage + 1, tags: tags) + Task { + await fetchPosts(page: currentPage + 1, tags: tags) - DispatchQueue.main.async { - self.currentPage += 1 - } - } + DispatchQueue.main.async { + self.currentPage += 1 + } } + } - func fetchAllTags(limit: Int = 100_000) { - guard provider != nil else { return } + func fetchAllTags(limit: Int = 100_000) { + guard provider != nil else { return } - Task { - guard let url = urlForTags(limit: limit) else { return } + Task { + guard let url = urlForTags(limit: limit) else { return } - do { - let (data, _) = try await URLSession.shared.data(from: url) + do { + let (data, _) = try await URLSession.shared.data(from: url) - if Task.isCancelled { return } + if Task.isCancelled { return } - DispatchQueue.main.async { - self.allTags = BooruTagXMLParser(data: data).parse().sorted { $0.count > $1.count } - } - } catch { - if (error as? URLError)?.code != .cancelled { - #if DEBUG - print("fetchAllTags: \(error)") - #endif - } - } + DispatchQueue.main.async { + self.allTags = BooruTagXMLParser(data: data).parse().sorted { $0.count > $1.count } } - } - - private func urlForPosts(page: Int, limit: Int, tags: [String]) -> URL? { - let tagString = tags.joined(separator: "+") - - switch provider { - case .yandere: - return URL(string: "https://yande.re/post.xml?page=\(page)&limit=\(limit)&tags=\(tagString)") - case .safebooru: - return URL(string: "https://safebooru.org/index.php?page=dapi&s=post&q=index&pid=\(page)&limit=\(limit)&tags=\(tagString)") - default: - return nil + } catch { + if (error as? URLError)?.code != .cancelled { + #if DEBUG + print("fetchAllTags: \(error)") + #endif } + } } - - private func urlForTags(limit: Int) -> URL? { - switch provider { - case .yandere: - URL(string: "https://yande.re/tag.xml?limit=\(limit)") - case .safebooru: - URL(string: "https://safebooru.org/index.php?page=dapi&s=tag&q=index&limit=\(limit)") - default: - nil - } + } + + private func urlForPosts(page: Int, limit: Int, tags: [String]) -> URL? { + let tagString = tags.joined(separator: "+") + + switch provider { + case .yandere: + return URL(string: "https://yande.re/post.xml?page=\(page)&limit=\(limit)&tags=\(tagString)") + case .safebooru: + return URL( + string: + "https://safebooru.org/index.php?page=dapi&s=post&q=index&pid=\(page)&limit=\(limit)&tags=\(tagString)" + ) + default: + return nil + } + } + + private func urlForTags(limit: Int) -> URL? { + switch provider { + case .yandere: + URL(string: "https://yande.re/tag.xml?limit=\(limit)") + case .safebooru: + URL(string: "https://safebooru.org/index.php?page=dapi&s=tag&q=index&limit=\(limit)") + default: + nil } + } } diff --git a/Sora/Data/Booru/BooruPost.swift b/Sora/Data/Booru/BooruPost.swift index 59b8952..7f995a3 100644 --- a/Sora/Data/Booru/BooruPost.swift +++ b/Sora/Data/Booru/BooruPost.swift @@ -1,27 +1,27 @@ import Foundation struct BooruPost: Identifiable, Hashable { - let id: String - let height: Int - let score: String - let fileURL: URL - let parentID: String - let sampleURL: URL - let sampleWidth: Int - let sampleHeight: Int - let previewURL: URL - let rating: String - let tags: [String] - let width: Int - let change: String - let md5: String - let creatorID: String - let hasChildren: Bool - let createdAt: Date - let status: String - let source: String - let hasNotes: Bool - let hasComments: Bool - let previewWidth: Int - let previewHeight: Int + let id: String + let height: Int + let score: String + let fileURL: URL + let parentID: String + let sampleURL: URL + let sampleWidth: Int + let sampleHeight: Int + let previewURL: URL + let rating: String + let tags: [String] + let width: Int + let change: String + let md5: String + let creatorID: String + let hasChildren: Bool + let createdAt: Date + let status: String + let source: String + let hasNotes: Bool + let hasComments: Bool + let previewWidth: Int + let previewHeight: Int } diff --git a/Sora/Data/Booru/BooruPostFileType.swift b/Sora/Data/Booru/BooruPostFileType.swift index 71d0352..62900db 100644 --- a/Sora/Data/Booru/BooruPostFileType.swift +++ b/Sora/Data/Booru/BooruPostFileType.swift @@ -1,5 +1,5 @@ enum BooruPostFileType: String, CaseIterable { - case original - case sample - case preview + case original + case sample + case preview } diff --git a/Sora/Data/Booru/BooruPostXMLParser.swift b/Sora/Data/Booru/BooruPostXMLParser.swift index 30207bc..79827aa 100644 --- a/Sora/Data/Booru/BooruPostXMLParser.swift +++ b/Sora/Data/Booru/BooruPostXMLParser.swift @@ -1,116 +1,122 @@ import Foundation class BooruPostXMLParser: NSObject, XMLParserDelegate { - private var posts: [BooruPost] = [] - private var currentPost: BooruPost? - private var parser: XMLParser - - init(data: Data) { - parser = XMLParser(data: data) - - super.init() - - parser.delegate = self + private var posts: [BooruPost] = [] + private var currentPost: BooruPost? + private var parser: XMLParser + + init(data: Data) { + parser = XMLParser(data: data) + + super.init() + + parser.delegate = self + } + + func parse() -> [BooruPost] { + parser.parse() + + return posts + } + + func parser( + _: XMLParser, didStartElement elementName: String, namespaceURI _: String?, + qualifiedName _: String?, attributes attributeDict: [String: String] = [:] + ) { + if elementName == "post" { + guard let id = attributeDict["id"], + let heightStr = attributeDict["height"], + let height = Int(heightStr), + let score = attributeDict["score"], + let fileUrl = attributeDict["file_url"], + let parentId = attributeDict["parent_id"], + let sampleUrl = attributeDict["sample_url"], + let sampleWidthStr = attributeDict["sample_width"], + let sampleWidth = Int(sampleWidthStr), + let sampleHeightStr = attributeDict["sample_height"], + let sampleHeight = Int(sampleHeightStr), + let previewUrl = attributeDict["preview_url"], + let rating = attributeDict["rating"], + let tags = attributeDict["tags"], + let widthStr = attributeDict["width"], + let width = Int(widthStr), + let change = attributeDict["change"], + let md5 = attributeDict["md5"], + let creatorId = attributeDict["creator_id"], + let hasChildrenStr = attributeDict["has_children"], + let createdAt = attributeDict["created_at"], + let status = attributeDict["status"], + let source = attributeDict["source"], + let previewWidthStr = attributeDict["preview_width"], + let previewWidth = Int(previewWidthStr), + let previewHeightStr = attributeDict["preview_height"], + let previewHeight = Int(previewHeightStr) + else { + return + } + + let hasNotesStr = attributeDict["has_notes"] ?? "false" + let hasCommentsStr = attributeDict["has_comments"] ?? "false" + + currentPost = BooruPost( + id: id, + height: height, + score: score, + fileURL: URL(string: fileUrl)!, + parentID: parentId, + sampleURL: URL(string: sampleUrl)!, + sampleWidth: sampleWidth, + sampleHeight: sampleHeight, + previewURL: URL(string: previewUrl)!, + rating: rating, + tags: tags.components(separatedBy: " ").filter { !$0.isEmpty }, + width: width, + change: change, + md5: md5, + creatorID: creatorId, + hasChildren: hasChildrenStr == "true", + createdAt: parseCreatedAt(createdAt)!, + status: status, + source: source, + hasNotes: hasNotesStr == "true", + hasComments: hasCommentsStr == "true", + previewWidth: previewWidth, + previewHeight: previewHeight + ) } + } - func parse() -> [BooruPost] { - parser.parse() - - return posts - } + func parser( + _: XMLParser, didEndElement elementName: String, namespaceURI _: String?, + qualifiedName _: String? + ) { + if elementName == "post", let post = currentPost { + posts.append(post) - func parser(_: XMLParser, didStartElement elementName: String, namespaceURI _: String?, qualifiedName _: String?, attributes attributeDict: [String: String] = [:]) { - if elementName == "post" { - guard let id = attributeDict["id"], - let heightStr = attributeDict["height"], - let height = Int(heightStr), - let score = attributeDict["score"], - let fileUrl = attributeDict["file_url"], - let parentId = attributeDict["parent_id"], - let sampleUrl = attributeDict["sample_url"], - let sampleWidthStr = attributeDict["sample_width"], - let sampleWidth = Int(sampleWidthStr), - let sampleHeightStr = attributeDict["sample_height"], - let sampleHeight = Int(sampleHeightStr), - let previewUrl = attributeDict["preview_url"], - let rating = attributeDict["rating"], - let tags = attributeDict["tags"], - let widthStr = attributeDict["width"], - let width = Int(widthStr), - let change = attributeDict["change"], - let md5 = attributeDict["md5"], - let creatorId = attributeDict["creator_id"], - let hasChildrenStr = attributeDict["has_children"], - let createdAt = attributeDict["created_at"], - let status = attributeDict["status"], - let source = attributeDict["source"], - let previewWidthStr = attributeDict["preview_width"], - let previewWidth = Int(previewWidthStr), - let previewHeightStr = attributeDict["preview_height"], - let previewHeight = Int(previewHeightStr) - else { - return - } - - let hasNotesStr = attributeDict["has_notes"] ?? "false" - let hasCommentsStr = attributeDict["has_comments"] ?? "false" - - currentPost = BooruPost( - id: id, - height: height, - score: score, - fileURL: URL(string: fileUrl)!, - parentID: parentId, - sampleURL: URL(string: sampleUrl)!, - sampleWidth: sampleWidth, - sampleHeight: sampleHeight, - previewURL: URL(string: previewUrl)!, - rating: rating, - tags: tags.components(separatedBy: " ").filter { !$0.isEmpty }, - width: width, - change: change, - md5: md5, - creatorID: creatorId, - hasChildren: hasChildrenStr == "true", - createdAt: parseCreatedAt(createdAt)!, - status: status, - source: source, - hasNotes: hasNotesStr == "true", - hasComments: hasCommentsStr == "true", - previewWidth: previewWidth, - previewHeight: previewHeight - ) - } + currentPost = nil } + } - func parser(_: XMLParser, didEndElement elementName: String, namespaceURI _: String?, qualifiedName _: String?) { - if elementName == "post", let post = currentPost { - posts.append(post) - - currentPost = nil - } + #if DEBUG + func parser(_: XMLParser, parseErrorOccurred parseError: any Error) { + print("parser: \(parseError)") } + #endif - #if DEBUG - func parser(_: XMLParser, parseErrorOccurred parseError: any Error) { - print("parser: \(parseError)") - } - #endif - - func parseCreatedAt(_ input: String) -> Date? { - let dateFormatter = DateFormatter() - - dateFormatter.dateFormat = "EEE MMM dd HH:mm:ss Z yyyy" - dateFormatter.locale = Locale(identifier: "en_US_POSIX") + func parseCreatedAt(_ input: String) -> Date? { + let dateFormatter = DateFormatter() - if let date = dateFormatter.date(from: input) { - return date - } + dateFormatter.dateFormat = "EEE MMM dd HH:mm:ss Z yyyy" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") - if let timestamp = Double(input) { - return Date(timeIntervalSince1970: timestamp) - } + if let date = dateFormatter.date(from: input) { + return date + } - return nil + if let timestamp = Double(input) { + return Date(timeIntervalSince1970: timestamp) } + + return nil + } } diff --git a/Sora/Data/Booru/BooruProvider.swift b/Sora/Data/Booru/BooruProvider.swift index 73bb4f2..994e4e3 100644 --- a/Sora/Data/Booru/BooruProvider.swift +++ b/Sora/Data/Booru/BooruProvider.swift @@ -1,13 +1,13 @@ enum BooruProvider: String, CaseIterable, Decodable, Encodable { - case yandere - case safebooru + case yandere + case safebooru - func formatted() -> String { - switch self { - case .yandere: - "yande.re" - case .safebooru: - "Safebooru" - } + func formatted() -> String { + switch self { + case .yandere: + "yande.re" + case .safebooru: + "Safebooru" } + } } diff --git a/Sora/Data/Booru/BooruTag.swift b/Sora/Data/Booru/BooruTag.swift index b98bc75..d603e9f 100644 --- a/Sora/Data/Booru/BooruTag.swift +++ b/Sora/Data/Booru/BooruTag.swift @@ -1,9 +1,9 @@ import Foundation struct BooruTag: Identifiable, Hashable { - let id: String - let name: String - let count: Int - let type: Int - let ambiguous: Bool + let id: String + let name: String + let count: Int + let type: Int + let ambiguous: Bool } diff --git a/Sora/Data/Booru/BooruTagXMLParser.swift b/Sora/Data/Booru/BooruTagXMLParser.swift index 0dfebc3..bba26c5 100644 --- a/Sora/Data/Booru/BooruTagXMLParser.swift +++ b/Sora/Data/Booru/BooruTagXMLParser.swift @@ -1,58 +1,64 @@ import Foundation class BooruTagXMLParser: NSObject, XMLParserDelegate { - private var tags: [BooruTag] = [] - private var currentTag: BooruTag? - private var parser: XMLParser + private var tags: [BooruTag] = [] + private var currentTag: BooruTag? + private var parser: XMLParser - init(data: Data) { - parser = XMLParser(data: data) + init(data: Data) { + parser = XMLParser(data: data) - super.init() + super.init() - parser.delegate = self - } + parser.delegate = self + } - func parse() -> [BooruTag] { - parser.parse() + func parse() -> [BooruTag] { + parser.parse() - return tags - } + return tags + } - func parser(_: XMLParser, didStartElement elementName: String, namespaceURI _: String?, qualifiedName _: String?, attributes attributeDict: [String: String] = [:]) { - if elementName == "tag" { - guard let id = attributeDict["id"], - let name = attributeDict["name"], - let countStr = attributeDict["count"], - let count = Int(countStr), - let typeStr = attributeDict["type"], - let type = Int(typeStr), - let ambiguousStr = attributeDict["ambiguous"] - else { - return - } - - currentTag = BooruTag( - id: id, - name: name, - count: count, - type: type, - ambiguous: ambiguousStr == "true" - ) - } + func parser( + _: XMLParser, didStartElement elementName: String, namespaceURI _: String?, + qualifiedName _: String?, attributes attributeDict: [String: String] = [:] + ) { + if elementName == "tag" { + guard let id = attributeDict["id"], + let name = attributeDict["name"], + let countStr = attributeDict["count"], + let count = Int(countStr), + let typeStr = attributeDict["type"], + let type = Int(typeStr), + let ambiguousStr = attributeDict["ambiguous"] + else { + return + } + + currentTag = BooruTag( + id: id, + name: name, + count: count, + type: type, + ambiguous: ambiguousStr == "true" + ) } + } - func parser(_: XMLParser, didEndElement elementName: String, namespaceURI _: String?, qualifiedName _: String?) { - if elementName == "tag", let tag = currentTag { - tags.append(tag) + func parser( + _: XMLParser, didEndElement elementName: String, namespaceURI _: String?, + qualifiedName _: String? + ) { + if elementName == "tag", let tag = currentTag { + tags.append(tag) - currentTag = nil - } + currentTag = nil } + } - #if DEBUG - func parser(_: XMLParser, parseErrorOccurred parseError: any Error) { - print(parseError) - } - #endif + #if DEBUG + func parser(_: XMLParser, parseErrorOccurred parseError: any Error) { + print(parseError) + } + #endif } diff --git a/Sora/Data/Settings/Bookmark.swift b/Sora/Data/Settings/Bookmark.swift index 16f1e50..84dd802 100644 --- a/Sora/Data/Settings/Bookmark.swift +++ b/Sora/Data/Settings/Bookmark.swift @@ -1,15 +1,15 @@ import Foundation struct Bookmark: Codable, Identifiable, Hashable { - let id: UUID - let tags: [String] - let createdAt: Date - let provider: BooruProvider + let id: UUID + let tags: [String] + let createdAt: Date + let provider: BooruProvider - init(id: UUID = UUID(), provider: BooruProvider, tags: [String]) { - createdAt = Date() - self.id = id - self.tags = tags - self.provider = provider - } + init(id: UUID = UUID(), provider: BooruProvider, tags: [String]) { + createdAt = Date() + self.id = id + self.tags = tags + self.provider = provider + } } diff --git a/Sora/Data/Settings/Settings.swift b/Sora/Data/Settings/Settings.swift index c70f1ba..efac028 100644 --- a/Sora/Data/Settings/Settings.swift +++ b/Sora/Data/Settings/Settings.swift @@ -1,81 +1,81 @@ import SwiftUI class Settings: ObservableObject { - #if DEBUG - @AppStorage("detailViewType") var detailViewType: BooruPostFileType = .sample - #else - @AppStorage("detailViewType") var detailViewType: BooruPostFileType = .original - #endif - @AppStorage("thumbnailType") var thumbnailType: BooruPostFileType = .preview - @AppStorage("searchSuggestions") var searchSuggestions: Bool = false - @AppStorage("columns") var columns: Int = 2 - @AppStorage("blurNSFWThumbnails") var blurNSFWThumbnails: Bool = true - @AppStorage("showNSFWPosts") var showNSFWPosts: Bool = false - @AppStorage("bookmarks") private var bookmarksData: Data = .init() - @AppStorage("preferredBooru") var preferredBooru: BooruProvider = .yandere - - var bookmarks: [Bookmark] { - get { - if let bookmarks = try? JSONDecoder().decode([Bookmark].self, from: bookmarksData) { - return bookmarks - } - - return [] - } - - set { - if let data = try? JSONEncoder().encode(newValue) { - bookmarksData = data - } - } + #if DEBUG + @AppStorage("detailViewType") var detailViewType: BooruPostFileType = .sample + #else + @AppStorage("detailViewType") var detailViewType: BooruPostFileType = .original + #endif + @AppStorage("thumbnailType") var thumbnailType: BooruPostFileType = .preview + @AppStorage("searchSuggestions") var searchSuggestions: Bool = false + @AppStorage("columns") var columns: Int = 2 + @AppStorage("blurNSFWThumbnails") var blurNSFWThumbnails: Bool = true + @AppStorage("showNSFWPosts") var showNSFWPosts: Bool = false + @AppStorage("bookmarks") private var bookmarksData: Data = .init() + @AppStorage("preferredBooru") var preferredBooru: BooruProvider = .yandere + + var bookmarks: [Bookmark] { + get { + if let bookmarks = try? JSONDecoder().decode([Bookmark].self, from: bookmarksData) { + return bookmarks + } + + return [] } - func resetToDefaults() { - #if DEBUG - detailViewType = .preview - #else - detailViewType = .original - #endif - thumbnailType = .preview - searchSuggestions = false - columns = 2 - blurNSFWThumbnails = true - showNSFWPosts = false + set { + if let data = try? JSONEncoder().encode(newValue) { + bookmarksData = data + } } + } - func addBookmark(provider: BooruProvider, tags: [String]) { - var currentBookmarks = bookmarks + func resetToDefaults() { + #if DEBUG + detailViewType = .preview + #else + detailViewType = .original + #endif + thumbnailType = .preview + searchSuggestions = false + columns = 2 + blurNSFWThumbnails = true + showNSFWPosts = false + } - currentBookmarks.append(Bookmark(provider: provider, tags: tags.map { $0.lowercased() })) + func addBookmark(provider: BooruProvider, tags: [String]) { + var currentBookmarks = bookmarks - bookmarks = currentBookmarks - } + currentBookmarks.append(Bookmark(provider: provider, tags: tags.map { $0.lowercased() })) - func removeBookmark(at index: IndexSet) { - var currentBookmarks = bookmarks + bookmarks = currentBookmarks + } - currentBookmarks.remove(atOffsets: index) + func removeBookmark(at index: IndexSet) { + var currentBookmarks = bookmarks - bookmarks = currentBookmarks - } + currentBookmarks.remove(atOffsets: index) - func removeBookmark(withTags tags: [String]) { - var currentBookmarks = bookmarks + bookmarks = currentBookmarks + } - currentBookmarks.removeAll { bookmark in - bookmark.tags.contains(where: tags.contains) - } + func removeBookmark(withTags tags: [String]) { + var currentBookmarks = bookmarks - bookmarks = currentBookmarks + currentBookmarks.removeAll { bookmark in + bookmark.tags.contains(where: tags.contains) } - func removeBookmark(withID: UUID) { - var currentBookmarks = bookmarks + bookmarks = currentBookmarks + } - currentBookmarks.removeAll { bookmark in - bookmark.id == withID - } + func removeBookmark(withID: UUID) { + var currentBookmarks = bookmarks - bookmarks = currentBookmarks + currentBookmarks.removeAll { bookmark in + bookmark.id == withID } + + bookmarks = currentBookmarks + } } diff --git a/Sora/Other/AsyncImageWithPreview.swift b/Sora/Other/AsyncImageWithPreview.swift index 43b31c4..48df44e 100644 --- a/Sora/Other/AsyncImageWithPreview.swift +++ b/Sora/Other/AsyncImageWithPreview.swift @@ -1,163 +1,167 @@ import SwiftUI struct AsyncImageWithPreview<Placeholder: View>: View { - var url: URL? - @Binding var loadingState: PostLoadingState - var finalLoadingState: PostLoadingState - var postURL: URL? - let placeholder: () -> Placeholder - @State private var currentScale: CGFloat = 1.0 - @State private var finalScale: CGFloat = 1.0 - @State private var currentOffset: CGSize = .zero - @State private var finalOffset: CGSize = .zero + var url: URL? + @Binding var loadingState: PostLoadingState + var finalLoadingState: PostLoadingState + var postURL: URL? + let placeholder: () -> Placeholder + @State private var currentScale: CGFloat = 1.0 + @State private var finalScale: CGFloat = 1.0 + @State private var currentOffset: CGSize = .zero + @State private var finalOffset: CGSize = .zero - init( - url: URL?, - loadingStage: Binding<PostLoadingState>, - finalLoadingState: PostLoadingState = .loadingFile, - postURL: URL? = nil, - @ViewBuilder placeholder: @escaping () -> Placeholder = { - GeometryReader { geometry in - ProgressView() - .frame(width: geometry.size.width, height: geometry.size.height) - .position(x: geometry.size.width / 2, y: geometry.size.height / 2) - .padding() - } - } - ) { - self.url = url - _loadingState = loadingStage - self.finalLoadingState = finalLoadingState - self.postURL = postURL - self.placeholder = placeholder + init( + url: URL?, + loadingStage: Binding<PostLoadingState>, + finalLoadingState: PostLoadingState = .loadingFile, + postURL: URL? = nil, + @ViewBuilder placeholder: @escaping () -> Placeholder = { + GeometryReader { geometry in + ProgressView() + .frame(width: geometry.size.width, height: geometry.size.height) + .position(x: geometry.size.width / 2, y: geometry.size.height / 2) + .padding() + } } + ) { + self.url = url + _loadingState = loadingStage + self.finalLoadingState = finalLoadingState + self.postURL = postURL + self.placeholder = placeholder + } - var body: some View { - GeometryReader { geometry in - AsyncImage(url: url) { image in - image - .resizable() - .scaledToFit() - .onAppear { - loadingState = finalLoadingState - } - .scaleEffect(finalScale * currentScale) - .offset(x: finalOffset.width + currentOffset.width, - y: finalOffset.height + currentOffset.height) - .frame(width: geometry.size.width, height: geometry.size.height) - .position(x: geometry.size.width / 2, y: geometry.size.height / 2) - .gesture( - DragGesture() - .onChanged { value in - let translation = value.translation - let newOffset = CGSize( - width: finalOffset.width + translation.width, - height: finalOffset.height + translation.height - ) - let scale = finalScale * currentScale - let imageWidth = geometry.size.width * scale - let imageHeight = geometry.size.height * scale - let maxX = max((imageWidth - geometry.size.width) / 2, 0) - let maxY = max((imageHeight - geometry.size.height) / 2, 0) - let clampedX = min(max(newOffset.width, -maxX), maxX) - let clampedY = min(max(newOffset.height, -maxY), maxY) + var body: some View { + GeometryReader { geometry in + AsyncImage(url: url) { image in + image + .resizable() + .scaledToFit() + .onAppear { + loadingState = finalLoadingState + } + .scaleEffect(finalScale * currentScale) + .offset( + x: finalOffset.width + currentOffset.width, + y: finalOffset.height + currentOffset.height + ) + .frame(width: geometry.size.width, height: geometry.size.height) + .position(x: geometry.size.width / 2, y: geometry.size.height / 2) + .gesture( + DragGesture() + .onChanged { value in + let translation = value.translation + let newOffset = CGSize( + width: finalOffset.width + translation.width, + height: finalOffset.height + translation.height + ) + let scale = finalScale * currentScale + let imageWidth = geometry.size.width * scale + let imageHeight = geometry.size.height * scale + let maxX = max((imageWidth - geometry.size.width) / 2, 0) + let maxY = max((imageHeight - geometry.size.height) / 2, 0) + let clampedX = min(max(newOffset.width, -maxX), maxX) + let clampedY = min(max(newOffset.height, -maxY), maxY) - currentOffset = CGSize( - width: clampedX - finalOffset.width, - height: clampedY - finalOffset.height - ) - } - .onEnded { value in - let translation = value.translation - var newOffset = CGSize( - width: finalOffset.width + translation.width, - height: finalOffset.height + translation.height - ) - let scale = finalScale * currentScale - let imageWidth = geometry.size.width * scale - let imageHeight = geometry.size.height * scale - let maxX = max((imageWidth - geometry.size.width) / 2, 0) - let maxY = max((imageHeight - geometry.size.height) / 2, 0) + currentOffset = CGSize( + width: clampedX - finalOffset.width, + height: clampedY - finalOffset.height + ) + } + .onEnded { value in + let translation = value.translation + var newOffset = CGSize( + width: finalOffset.width + translation.width, + height: finalOffset.height + translation.height + ) + let scale = finalScale * currentScale + let imageWidth = geometry.size.width * scale + let imageHeight = geometry.size.height * scale + let maxX = max((imageWidth - geometry.size.width) / 2, 0) + let maxY = max((imageHeight - geometry.size.height) / 2, 0) - newOffset.width = min(max(newOffset.width, -maxX), maxX) - newOffset.height = min(max(newOffset.height, -maxY), maxY) - finalOffset = newOffset - currentOffset = .zero - } - ) - .simultaneousGesture( - MagnificationGesture() - .onChanged { value in - currentScale = value - } - .onEnded { _ in - finalScale *= currentScale - currentScale = 1.0 + newOffset.width = min(max(newOffset.width, -maxX), maxX) + newOffset.height = min(max(newOffset.height, -maxY), maxY) + finalOffset = newOffset + currentOffset = .zero + } + ) + .simultaneousGesture( + MagnificationGesture() + .onChanged { value in + currentScale = value + } + .onEnded { _ in + finalScale *= currentScale + currentScale = 1.0 - let scale = finalScale - let imageWidth = geometry.size.width * scale - let imageHeight = geometry.size.height * scale - let maxX = max((imageWidth - geometry.size.width) / 2, 0) - let maxY = max((imageHeight - geometry.size.height) / 2, 0) + let scale = finalScale + let imageWidth = geometry.size.width * scale + let imageHeight = geometry.size.height * scale + let maxX = max((imageWidth - geometry.size.width) / 2, 0) + let maxY = max((imageHeight - geometry.size.height) / 2, 0) - finalOffset.width = min(max(finalOffset.width, -maxX), maxX) - finalOffset.height = min(max(finalOffset.height, -maxY), maxY) - } - ) - .highPriorityGesture( - TapGesture(count: 2) - .onEnded { - withAnimation { - finalScale = 1.0 - currentScale = 1.0 - finalOffset = .zero - currentOffset = .zero - } - } - ) - .contextMenu { - #if os(iOS) - Button { - guard let url else { return } + finalOffset.width = min(max(finalOffset.width, -maxX), maxX) + finalOffset.height = min(max(finalOffset.height, -maxY), maxY) + } + ) + .highPriorityGesture( + TapGesture(count: 2) + .onEnded { + withAnimation { + finalScale = 1.0 + currentScale = 1.0 + finalOffset = .zero + currentOffset = .zero + } + } + ) + .contextMenu { + #if os(iOS) + Button { + guard let url else { return } - URLSession.shared.dataTask(with: url) { data, _, _ in - guard let data, let uiImage = UIImage(data: data) else { return } + URLSession.shared.dataTask(with: url) { data, _, _ in + guard let data, let uiImage = UIImage(data: data) else { return } - UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil) - }.resume() - } label: { - Label("Save Image", systemImage: "square.and.arrow.down") - } - #endif + UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil) + }.resume() + } label: { + Label("Save Image", systemImage: "square.and.arrow.down") + } + #endif - #if os(iOS) - Button { - let activityViewController = UIActivityViewController(activityItems: [url ?? URL(string: "")!], applicationActivities: nil) + #if os(iOS) + Button { + let activityViewController = UIActivityViewController( + activityItems: [url ?? URL(string: "")!], applicationActivities: nil) - UIApplication.shared.windows.first?.rootViewController?.present(activityViewController, animated: true) - } label: { - Label("Share Image", systemImage: "square.and.arrow.up") - } - #endif + UIApplication.shared.windows.first?.rootViewController?.present( + activityViewController, animated: true) + } label: { + Label("Share Image", systemImage: "square.and.arrow.up") + } + #endif - if let url = postURL { - Button { - #if os(iOS) - UIApplication.shared.open(url) - #else - NSWorkspace.shared.open(url) - #endif - } label: { - Label("Open in Safari", systemImage: "safari") - } - } - } - } placeholder: { - placeholder() - .onAppear { - loadingState = .loadingPreview - } + if let url = postURL { + Button { + #if os(iOS) + UIApplication.shared.open(url) + #else + NSWorkspace.shared.open(url) + #endif + } label: { + Label("Open in Safari", systemImage: "safari") + } } - } + } + } placeholder: { + placeholder() + .onAppear { + loadingState = .loadingPreview + } + } } + } } diff --git a/Sora/Other/PostLoadingStage.swift b/Sora/Other/PostLoadingStage.swift index 4a8c7f3..800b4dc 100644 --- a/Sora/Other/PostLoadingStage.swift +++ b/Sora/Other/PostLoadingStage.swift @@ -1,5 +1,5 @@ enum PostLoadingState { - case loadingPreview - case loadingFile - case loaded + case loadingPreview + case loadingFile + case loaded } diff --git a/Sora/SoraApp.swift b/Sora/SoraApp.swift index a080c4c..70a01b5 100644 --- a/Sora/SoraApp.swift +++ b/Sora/SoraApp.swift @@ -2,34 +2,34 @@ import SwiftUI @main struct SoraApp: App { - @StateObject private var settings = Settings() + @StateObject private var settings = Settings() - var body: some Scene { - WindowGroup { - MainView() - .environmentObject(settings) - } - - #if os(macOS) - SwiftUI.Settings { - SettingsView() - .environmentObject(settings) - } - #endif + var body: some Scene { + WindowGroup { + MainView() + .environmentObject(settings) } + + #if os(macOS) + SwiftUI.Settings { + SettingsView() + .environmentObject(settings) + } + #endif + } } struct SoraApp_Previews: PreviewProvider { - static var previews: some View { - MainView() - .environmentObject(Settings()) - .previewDevice(PreviewDevice(rawValue: "iPhone 16 Pro Max")) - .previewDisplayName("iPhone") + static var previews: some View { + MainView() + .environmentObject(Settings()) + .previewDevice(PreviewDevice(rawValue: "iPhone 16 Pro Max")) + .previewDisplayName("iPhone") - MainView() - .environmentObject(Settings()) - .previewDevice(PreviewDevice(rawValue: "My Mac")) - .previewDisplayName("Mac") - .previewLayout(.fixed(width: 750, height: 800)) - } + MainView() + .environmentObject(Settings()) + .previewDevice(PreviewDevice(rawValue: "My Mac")) + .previewDisplayName("Mac") + .previewLayout(.fixed(width: 750, height: 800)) + } } diff --git a/Sora/Views/Bookmarks/BookmarkListItemView.swift b/Sora/Views/Bookmarks/BookmarkListItemView.swift index 6d62893..6c46416 100644 --- a/Sora/Views/Bookmarks/BookmarkListItemView.swift +++ b/Sora/Views/Bookmarks/BookmarkListItemView.swift @@ -1,39 +1,39 @@ import SwiftUI struct BookmarkListItemView: View { - @EnvironmentObject var settings: Settings - var bookmark: Bookmark + @EnvironmentObject var settings: Settings + var bookmark: Bookmark - var body: some View { - VStack(alignment: .leading) { - HStack { - Text(bookmark.tags.joined(separator: ", ")) + var body: some View { + VStack(alignment: .leading) { + HStack { + Text(bookmark.tags.joined(separator: ", ")) - #if os(macOS) - Spacer() + #if os(macOS) + Spacer() - Button { - settings.removeBookmark(withID: bookmark.id) - } label: { - Image(systemName: "trash") - } - #endif - } + Button { + settings.removeBookmark(withID: bookmark.id) + } label: { + Image(systemName: "trash") + } + #endif + } - HStack { - Text(bookmark.createdAt, style: .date) - .font(.caption) - .foregroundStyle(Color.secondary) + HStack { + Text(bookmark.createdAt, style: .date) + .font(.caption) + .foregroundStyle(Color.secondary) - Spacer() + Spacer() - Text(bookmark.provider.formatted()) - .font(.caption) - .foregroundStyle(Color.secondary) - } - } - #if os(macOS) - .padding() - #endif + Text(bookmark.provider.formatted()) + .font(.caption) + .foregroundStyle(Color.secondary) + } } + #if os(macOS) + .padding() + #endif + } } diff --git a/Sora/Views/Bookmarks/BookmarksView.swift b/Sora/Views/Bookmarks/BookmarksView.swift index 83eda0e..b36ee45 100644 --- a/Sora/Views/Bookmarks/BookmarksView.swift +++ b/Sora/Views/Bookmarks/BookmarksView.swift @@ -1,69 +1,72 @@ import SwiftUI struct BookmarksView: View { - @EnvironmentObject var settings: Settings - @EnvironmentObject var manager: BooruManager - @Binding var selectedTab: Int - @State private var bookmarksSearchText: String = "" + @EnvironmentObject var settings: Settings + @EnvironmentObject var manager: BooruManager + @Binding var selectedTab: Int + @State private var bookmarksSearchText: String = "" - var filteredBookmarks: [Bookmark] { - guard !bookmarksSearchText.isEmpty else { - return settings.bookmarks - } - - return settings.bookmarks - .filter { $0.tags.joined(separator: " ").lowercased().contains(bookmarksSearchText.lowercased()) } + var filteredBookmarks: [Bookmark] { + guard !bookmarksSearchText.isEmpty else { + return settings.bookmarks } - var body: some View { - NavigationStack { - VStack { - if settings.bookmarks.isEmpty { - ContentUnavailableView("No Bookmarks", - systemImage: "bookmark", - description: Text("Add a bookmark by tapping the bookmark button on a search page.")) - } else { - List { - if filteredBookmarks.isEmpty, !bookmarksSearchText.isEmpty { - Text("No bookmarks match your search") - } + return settings.bookmarks + .filter { + $0.tags.joined(separator: " ").lowercased().contains(bookmarksSearchText.lowercased()) + } + } + + var body: some View { + NavigationStack { + VStack { + if settings.bookmarks.isEmpty { + ContentUnavailableView( + "No Bookmarks", + systemImage: "bookmark", + description: Text("Add a bookmark by tapping the bookmark button on a search page.")) + } else { + List { + if filteredBookmarks.isEmpty, !bookmarksSearchText.isEmpty { + Text("No bookmarks match your search") + } - ForEach( - filteredBookmarks, - id: \.self - ) { bookmark in - Button(action: { - let previousProvider = settings.preferredBooru + ForEach( + filteredBookmarks, + id: \.self + ) { bookmark in + Button(action: { + let previousProvider = settings.preferredBooru - settings.preferredBooru = bookmark.provider - manager.searchText = bookmark.tags.joined(separator: " ") - selectedTab = 0 + settings.preferredBooru = bookmark.provider + manager.searchText = bookmark.tags.joined(separator: " ") + selectedTab = 0 - if previousProvider == settings.preferredBooru { - manager.performSearch() - } - }) { - BookmarkListItemView(bookmark: bookmark) - } - #if os(macOS) - .buttonStyle(.plain) - #endif - } - .onDelete(perform: settings.removeBookmark) - } - #if os(macOS) - .listStyle(.plain) - #endif + if previousProvider == settings.preferredBooru { + manager.performSearch() } + }) { + BookmarkListItemView(bookmark: bookmark) + } + #if os(macOS) + .buttonStyle(.plain) + #endif } + .onDelete(perform: settings.removeBookmark) + } + #if os(macOS) + .listStyle(.plain) + #endif } - .navigationTitle("Bookmarks") - .searchable(text: $bookmarksSearchText) + } } + .navigationTitle("Bookmarks") + .searchable(text: $bookmarksSearchText) + } } #Preview { - BookmarksView(selectedTab: .constant(1)) - .environmentObject(Settings()) - .environmentObject(BooruManager(.yandere)) + BookmarksView(selectedTab: .constant(1)) + .environmentObject(Settings()) + .environmentObject(BooruManager(.yandere)) } diff --git a/Sora/Views/ContentView.swift b/Sora/Views/ContentView.swift index d6473ab..fab1e14 100644 --- a/Sora/Views/ContentView.swift +++ b/Sora/Views/ContentView.swift @@ -1,48 +1,48 @@ import SwiftUI struct ContentView: View { - @EnvironmentObject var manager: BooruManager - @State private var selectedTabIndex: Int = 1 - @State private var tabs = [ - "Posts", - "Bookmarks", - ] + @EnvironmentObject var manager: BooruManager + @State private var selectedTabIndex: Int = 1 + @State private var tabs = [ + "Posts", + "Bookmarks", + ] - var body: some View { - #if os(macOS) - NavigationSplitView { - List(selection: $selectedTabIndex) { - ForEach(Array(tabs.enumerated()), id: \.offset) { index, element in - NavigationLink(value: index) { - Text(element) - } - } - } - } content: { - if selectedTabIndex == 1 { - BookmarksView(selectedTab: $selectedTabIndex) - } else { - PostGridView( - manager: manager - ) - } - } detail: { - if let post = manager.selectedPost { - PostDetailsView(post: post) - } else { - Text("Select a Post") - .foregroundColor(.secondary) - } + var body: some View { + #if os(macOS) + NavigationSplitView { + List(selection: $selectedTabIndex) { + ForEach(Array(tabs.enumerated()), id: \.offset) { index, element in + NavigationLink(value: index) { + Text(element) } - #else - NavigationStack { - PostGridView(manager: manager) - } - #endif - } + } + } + } content: { + if selectedTabIndex == 1 { + BookmarksView(selectedTab: $selectedTabIndex) + } else { + PostGridView( + manager: manager + ) + } + } detail: { + if let post = manager.selectedPost { + PostDetailsView(post: post) + } else { + Text("Select a Post") + .foregroundColor(.secondary) + } + } + #else + NavigationStack { + PostGridView(manager: manager) + } + #endif + } } #Preview { - ContentView() - .environmentObject(Settings()) + ContentView() + .environmentObject(Settings()) } diff --git a/Sora/Views/MainView.swift b/Sora/Views/MainView.swift index 82367c7..79ac1a3 100644 --- a/Sora/Views/MainView.swift +++ b/Sora/Views/MainView.swift @@ -1,67 +1,67 @@ import SwiftUI struct MainView: View { - @EnvironmentObject var settings: Settings - @State private var selectedTab: Int = 0 - @State private var manager = BooruManager(.yandere) + @EnvironmentObject var settings: Settings + @State private var selectedTab: Int = 0 + @State private var manager = BooruManager(.yandere) - var body: some View { - platformSpecificContent - .environmentObject(settings) - .environmentObject(manager) - .onChange(of: settings.preferredBooru) { _, newState in - updateManager(newState) - } - .onAppear(perform: initialiseManager) - } + var body: some View { + platformSpecificContent + .environmentObject(settings) + .environmentObject(manager) + .onChange(of: settings.preferredBooru) { _, newState in + updateManager(newState) + } + .onAppear(perform: initialiseManager) + } - @ViewBuilder - private var platformSpecificContent: some View { - #if os(macOS) - ContentView() - #else - TabView(selection: $selectedTab) { - ContentView() - .tabItem { Label("Posts", systemImage: "rectangle.stack") } - .tag(0) + @ViewBuilder + private var platformSpecificContent: some View { + #if os(macOS) + ContentView() + #else + TabView(selection: $selectedTab) { + ContentView() + .tabItem { Label("Posts", systemImage: "rectangle.stack") } + .tag(0) - NavigationStack { - BookmarksView(selectedTab: $selectedTab) - } - .tabItem { Label("Bookmarks", systemImage: "bookmark") } - .tag(1) + NavigationStack { + BookmarksView(selectedTab: $selectedTab) + } + .tabItem { Label("Bookmarks", systemImage: "bookmark") } + .tag(1) - NavigationStack { - SettingsView() - } - .tabItem { Label("Settings", systemImage: "gear") } - .tag(2) - } - #endif - } + NavigationStack { + SettingsView() + } + .tabItem { Label("Settings", systemImage: "gear") } + .tag(2) + } + #endif + } - private func updateManager(_ provider: BooruProvider) { - let previousSearchText = manager.searchText + private func updateManager(_ provider: BooruProvider) { + let previousSearchText = manager.searchText - manager = BooruManager(provider) - manager.searchText = previousSearchText + manager = BooruManager(provider) + manager.searchText = previousSearchText - Task { - if manager.searchText.isEmpty { - await manager.fetchPosts() - } else { - manager.performSearch() - } - } + Task { + if manager.searchText.isEmpty { + await manager.fetchPosts() + } else { + manager.performSearch() + } } + } - private func initialiseManager() { - manager = BooruManager(settings.preferredBooru) + private func initialiseManager() { + manager = BooruManager(settings.preferredBooru) - Task { - if manager.posts.isEmpty { - await manager.fetchPosts() - } - } + Task { + if manager.posts.isEmpty { + await manager.fetchPosts() + } } + } } diff --git a/Sora/Views/Post/PostDetailsView.swift b/Sora/Views/Post/PostDetailsView.swift index 0e2566d..133fa34 100644 --- a/Sora/Views/Post/PostDetailsView.swift +++ b/Sora/Views/Post/PostDetailsView.swift @@ -1,83 +1,83 @@ import SwiftUI struct PostDetailsView: View { - @EnvironmentObject var settings: Settings - let post: BooruPost - @State var loadingStage: PostLoadingState = .loadingPreview - private var imageURL: URL? { - switch settings.detailViewType { - case .preview: - post.previewURL - case .sample: - post.sampleURL - case .original: - post.fileURL - } + @EnvironmentObject var settings: Settings + let post: BooruPost + @State var loadingStage: PostLoadingState = .loadingPreview + private var imageURL: URL? { + switch settings.detailViewType { + case .preview: + post.previewURL + case .sample: + post.sampleURL + case .original: + post.fileURL } + } - var body: some View { - VStack(spacing: 0) { - AsyncImageWithPreview( - url: imageURL, - loadingStage: $loadingStage, - finalLoadingState: .loaded, - postURL: URL(string: "https://yande.re/post/show/\(post.id)")! - ) { - AsyncImageWithPreview( - url: post.previewURL, - loadingStage: $loadingStage - ) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) - .id(post.previewURL) - } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) - .id(imageURL) - .padding(0) - .zIndex(0) + var body: some View { + VStack(spacing: 0) { + AsyncImageWithPreview( + url: imageURL, + loadingStage: $loadingStage, + finalLoadingState: .loaded, + postURL: URL(string: "https://yande.re/post/show/\(post.id)")! + ) { + AsyncImageWithPreview( + url: post.previewURL, + loadingStage: $loadingStage + ) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .id(post.previewURL) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .id(imageURL) + .padding(0) + .zIndex(0) - VStack(spacing: 5) { - HStack { - Text(post.tags.joined(separator: ", ")) - } - .frame(maxWidth: .infinity, alignment: .leading) + VStack(spacing: 5) { + HStack { + Text(post.tags.joined(separator: ", ")) + } + .frame(maxWidth: .infinity, alignment: .leading) - HStack { - Text( - post.createdAt.formatted() - ) - .frame(maxWidth: .infinity, alignment: .leading) + HStack { + Text( + post.createdAt.formatted() + ) + .frame(maxWidth: .infinity, alignment: .leading) - Group { - switch loadingStage { - case .loadingPreview: - Text("Loading preview …") - case .loadingFile: - Text("Loading \(settings.detailViewType.rawValue) …") - case .loaded: - EmptyView() - } - } - .padding(.trailing, 5) - } - .frame(maxWidth: .infinity, alignment: .leading) - .foregroundStyle(.secondary) + Group { + switch loadingStage { + case .loadingPreview: + Text("Loading preview …") + case .loadingFile: + Text("Loading \(settings.detailViewType.rawValue) …") + case .loaded: + EmptyView() } - .padding(.horizontal, 10) - .padding(.vertical, 10 / 1.33) - .textSelection(.enabled) - .font(.footnote) - #if os(iOS) - .background(.ultraThinMaterial) - #else - .background(.opacity(0.1)) - #endif - .zIndex(1) + } + .padding(.trailing, 5) } - .navigationTitle("Details") - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - .toolbarBackground(.visible, for: .navigationBar) - .toolbarBackground(.ultraThinMaterial, for: .navigationBar) - #endif + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundStyle(.secondary) + } + .padding(.horizontal, 10) + .padding(.vertical, 10 / 1.33) + .textSelection(.enabled) + .font(.footnote) + #if os(iOS) + .background(.ultraThinMaterial) + #else + .background(.opacity(0.1)) + #endif + .zIndex(1) } + .navigationTitle("Details") + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(.visible, for: .navigationBar) + .toolbarBackground(.ultraThinMaterial, for: .navigationBar) + #endif + } } diff --git a/Sora/Views/Post/PostGridBookmarkButtonView.swift b/Sora/Views/Post/PostGridBookmarkButtonView.swift index 90336f9..f23482f 100644 --- a/Sora/Views/Post/PostGridBookmarkButtonView.swift +++ b/Sora/Views/Post/PostGridBookmarkButtonView.swift @@ -1,28 +1,33 @@ import SwiftUI struct PostGridBookmarkButtonView: View { - @EnvironmentObject private var manager: BooruManager - @EnvironmentObject private var settings: Settings + @EnvironmentObject private var manager: BooruManager + @EnvironmentObject private var settings: Settings - var contained: Bool { - settings.bookmarks - .contains(where: { $0.tags == manager.tags.map { $0.lowercased() } && $0.provider == manager.provider ?? settings.preferredBooru }) - } + var contained: Bool { + settings.bookmarks + .contains(where: { + $0.tags == manager.tags.map { $0.lowercased() } + && $0.provider == manager.provider ?? settings.preferredBooru + }) + } - var body: some View { - Button( - action: { - contained ? settings - .removeBookmark(withTags: manager.tags) : settings - .addBookmark( - provider: manager.provider ?? settings.preferredBooru, - tags: manager.tags - ) - }) { - Label("Bookmark", systemImage: - contained ? - "bookmark.fill" : - "bookmark") - } - } + var body: some View { + Button( + action: { + contained + ? settings + .removeBookmark(withTags: manager.tags) + : settings + .addBookmark( + provider: manager.provider ?? settings.preferredBooru, + tags: manager.tags + ) + }) { + Label( + "Bookmark", + systemImage: + contained ? "bookmark.fill" : "bookmark") + } + } } diff --git a/Sora/Views/Post/PostGridView.swift b/Sora/Views/Post/PostGridView.swift index 89c762a..842d0a6 100644 --- a/Sora/Views/Post/PostGridView.swift +++ b/Sora/Views/Post/PostGridView.swift @@ -2,98 +2,99 @@ import SwiftUI import WaterfallGrid struct PostGridView: View { - @EnvironmentObject var settings: Settings - @ObservedObject var manager: BooruManager - @Environment(\.isSearching) private var isSearching + @EnvironmentObject var settings: Settings + @ObservedObject var manager: BooruManager + @Environment(\.isSearching) private var isSearching - var filteredPosts: [BooruPost] { - (settings.showNSFWPosts ? manager.posts : manager.posts.filter { $0.rating == "s" || $0.rating == "q" }) - .sorted(by: { $0.id > $1.id }) - } + var filteredPosts: [BooruPost] { + (settings.showNSFWPosts + ? manager.posts : manager.posts.filter { $0.rating == "s" || $0.rating == "q" }) + .sorted(by: { $0.id > $1.id }) + } - var body: some View { - ScrollViewReader { _ in - ScrollView { - if filteredPosts.isEmpty { - ProgressView() - .padding() - } + var body: some View { + ScrollViewReader { _ in + ScrollView { + if filteredPosts.isEmpty { + ProgressView() + .padding() + } - WaterfallGrid(filteredPosts, id: \.id) { post in - Group { - #if os(macOS) - Button { - manager.selectedPost = post - } label: { - PostView( - post: post, - manager: manager, - posts: filteredPosts - ) - } - .buttonStyle(PlainButtonStyle()) - #else - NavigationLink(value: post) { - PostView( - post: post, - manager: manager, - posts: filteredPosts - ) - } - #endif - } - } - .gridStyle(columns: settings.columns) - } - .searchable(text: $manager.searchText, prompt: "Tags") - .searchSuggestions { - if settings.searchSuggestions { - SearchSuggestionsView( - tags: manager.allTags, - searchText: $manager.searchText - ) - } - } - .onSubmit(of: .search, manager.performSearch) - .navigationDestination(for: BooruPost.self) { post in - PostDetailsView(post: post) - } - .onChange(of: manager.searchText) { _, _ in - if manager.searchText.isEmpty, !isSearching { - Task { - manager.performSearch() - } - } + WaterfallGrid(filteredPosts, id: \.id) { post in + Group { + #if os(macOS) + Button { + manager.selectedPost = post + } label: { + PostView( + post: post, + manager: manager, + posts: filteredPosts + ) + } + .buttonStyle(PlainButtonStyle()) + #else + NavigationLink(value: post) { + PostView( + post: post, + manager: manager, + posts: filteredPosts + ) + } + #endif + } + } + .gridStyle(columns: settings.columns) + } + .searchable(text: $manager.searchText, prompt: "Tags") + .searchSuggestions { + if settings.searchSuggestions { + SearchSuggestionsView( + tags: manager.allTags, + searchText: $manager.searchText + ) + } + } + .onSubmit(of: .search, manager.performSearch) + .navigationDestination(for: BooruPost.self) { post in + PostDetailsView(post: post) + } + .onChange(of: manager.searchText) { _, _ in + if manager.searchText.isEmpty, !isSearching { + Task { + manager.performSearch() + } + } + } + .toolbar { + #if os(macOS) + ToolbarItem { + Button(action: { + Task { + await manager.fetchPosts(page: 1, tags: manager.tags, replace: true) + } + }) { + Label("Refresh", systemImage: "arrow.clockwise") } - .toolbar { - #if os(macOS) - ToolbarItem { - Button(action: { - Task { - await manager.fetchPosts(page: 1, tags: manager.tags, replace: true) - } - }) { - Label("Refresh", systemImage: "arrow.clockwise") - } - } - #endif + } + #endif - if !manager.tags.isEmpty { - #if os(macOS) - ToolbarItem { - PostGridBookmarkButtonView() - } - #else - ToolbarItem(placement: .bottomBar) { - PostGridBookmarkButtonView() - } - #endif - } + if !manager.tags.isEmpty { + #if os(macOS) + ToolbarItem { + PostGridBookmarkButtonView() } - .navigationTitle("Posts") - .refreshable { - await manager.fetchPosts(page: 1, tags: manager.tags, replace: true) + #else + ToolbarItem(placement: .bottomBar) { + PostGridBookmarkButtonView() } + #endif } + } + .navigationTitle("Posts") + .refreshable { + await manager.fetchPosts(page: 1, tags: manager.tags, replace: true) + } } + } } diff --git a/Sora/Views/Post/PostView.swift b/Sora/Views/Post/PostView.swift index ce46152..9b92f4f 100644 --- a/Sora/Views/Post/PostView.swift +++ b/Sora/Views/Post/PostView.swift @@ -1,40 +1,43 @@ import SwiftUI struct PostView: View { - @EnvironmentObject var settings: Settings - let post: BooruPost - @ObservedObject var manager: BooruManager - let posts: [BooruPost] - private var thumbnailURL: URL? { - switch settings.thumbnailType { - case .preview: - post.previewURL - case .sample: - post.sampleURL - case .original: - post.fileURL - } + @EnvironmentObject var settings: Settings + let post: BooruPost + @ObservedObject var manager: BooruManager + let posts: [BooruPost] + private var thumbnailURL: URL? { + switch settings.thumbnailType { + case .preview: + post.previewURL + case .sample: + post.sampleURL + case .original: + post.fileURL } + } - var body: some View { - VStack { - AsyncImage(url: thumbnailURL) { image in - image - .resizable() - .aspectRatio(contentMode: .fit) - .onScrollVisibilityChange { visible in - if post == posts.last, !manager.endOfData, visible { - Task { - manager.loadNextPage() - } - } - } - .blur(radius: settings.blurNSFWThumbnails ? (post.rating != "s" && post.rating != "q") ? 10 : 0 : 0) - .animation(.default, value: settings.blurNSFWThumbnails) - } placeholder: { - ProgressView() - .padding() + var body: some View { + VStack { + AsyncImage(url: thumbnailURL) { image in + image + .resizable() + .aspectRatio(contentMode: .fit) + .onScrollVisibilityChange { visible in + if post == posts.last, !manager.endOfData, visible { + Task { + manager.loadNextPage() + } } - } + } + .blur( + radius: settings.blurNSFWThumbnails + ? (post.rating != "s" && post.rating != "q") ? 10 : 0 : 0 + ) + .animation(.default, value: settings.blurNSFWThumbnails) + } placeholder: { + ProgressView() + .padding() + } } + } } diff --git a/Sora/Views/SearchSuggestionsView.swift b/Sora/Views/SearchSuggestionsView.swift index b9c3133..91f1f77 100644 --- a/Sora/Views/SearchSuggestionsView.swift +++ b/Sora/Views/SearchSuggestionsView.swift @@ -1,21 +1,23 @@ import SwiftUI struct SearchSuggestionsView: View { - var tags: [BooruTag] - @Binding var searchText: String - var lastSearchTag: String { - String(searchText.split(separator: " ").last ?? "") - } + var tags: [BooruTag] + @Binding var searchText: String + var lastSearchTag: String { + String(searchText.split(separator: " ").last ?? "") + } - var body: some View { - ForEach( - tags.filter { $0.name.lowercased().contains(lastSearchTag) - }) { suggestion in - Button { - searchText.replaceSubrange(searchText.range(of: lastSearchTag)!, with: suggestion.name) - } label: { - Text(suggestion.name) - } - } + var body: some View { + ForEach( + tags.filter { + $0.name.lowercased().contains(lastSearchTag) + } + ) { suggestion in + Button { + searchText.replaceSubrange(searchText.range(of: lastSearchTag)!, with: suggestion.name) + } label: { + Text(suggestion.name) + } } + } } diff --git a/Sora/Views/Settings/SettingsAttributionsView.swift b/Sora/Views/Settings/SettingsAttributionsView.swift index 28481fd..3c94c4e 100644 --- a/Sora/Views/Settings/SettingsAttributionsView.swift +++ b/Sora/Views/Settings/SettingsAttributionsView.swift @@ -1,9 +1,9 @@ import SwiftUI struct SettingsAttributionsView: View { - var body: some View { - Text("Rabbit SVG created by Kim Sun Young") - .fontWeight(.light) - .foregroundColor(.secondary) - } + var body: some View { + Text("Rabbit SVG created by Kim Sun Young") + .fontWeight(.light) + .foregroundColor(.secondary) + } } diff --git a/Sora/Views/Settings/SettingsDetailsView.swift b/Sora/Views/Settings/SettingsDetailsView.swift index 6b9be8d..713577c 100644 --- a/Sora/Views/Settings/SettingsDetailsView.swift +++ b/Sora/Views/Settings/SettingsDetailsView.swift @@ -1,13 +1,13 @@ import SwiftUI struct SettingsDetailsView: View { - @EnvironmentObject var settings: Settings + @EnvironmentObject var settings: Settings - var body: some View { - Picker("Detail View Type", selection: $settings.detailViewType) { - ForEach(BooruPostFileType.allCases, id: \.self) { type in - Text(type.rawValue.capitalized).tag(type) - } - } + var body: some View { + Picker("Detail View Type", selection: $settings.detailViewType) { + ForEach(BooruPostFileType.allCases, id: \.self) { type in + Text(type.rawValue.capitalized).tag(type) + } } + } } diff --git a/Sora/Views/Settings/SettingsProviderView.swift b/Sora/Views/Settings/SettingsProviderView.swift index 0829497..907450d 100644 --- a/Sora/Views/Settings/SettingsProviderView.swift +++ b/Sora/Views/Settings/SettingsProviderView.swift @@ -1,13 +1,13 @@ import SwiftUI struct SettingsProviderView: View { - @EnvironmentObject var settings: Settings + @EnvironmentObject var settings: Settings - var body: some View { - Picker("Provider", selection: $settings.preferredBooru) { - ForEach(BooruProvider.allCases, id: \.self) { type in - Text(type.formatted()).tag(type) - } - } + var body: some View { + Picker("Provider", selection: $settings.preferredBooru) { + ForEach(BooruProvider.allCases, id: \.self) { type in + Text(type.formatted()).tag(type) + } } + } } diff --git a/Sora/Views/Settings/SettingsSearchView.swift b/Sora/Views/Settings/SettingsSearchView.swift index 1052abf..63be2f1 100644 --- a/Sora/Views/Settings/SettingsSearchView.swift +++ b/Sora/Views/Settings/SettingsSearchView.swift @@ -1,9 +1,9 @@ import SwiftUI struct SettingsSearchView: View { - @EnvironmentObject var settings: Settings + @EnvironmentObject var settings: Settings - var body: some View { - Toggle("Suggest Search Tags", isOn: $settings.searchSuggestions) - } + var body: some View { + Toggle("Suggest Search Tags", isOn: $settings.searchSuggestions) + } } diff --git a/Sora/Views/Settings/SettingsThumbnailsView.swift b/Sora/Views/Settings/SettingsThumbnailsView.swift index 6503e19..6631987 100644 --- a/Sora/Views/Settings/SettingsThumbnailsView.swift +++ b/Sora/Views/Settings/SettingsThumbnailsView.swift @@ -1,31 +1,31 @@ import SwiftUI struct SettingsThumbnailsView: View { - @EnvironmentObject var settings: Settings + @EnvironmentObject var settings: Settings - var body: some View { - Picker("Thumbnail Type", selection: $settings.thumbnailType) { - ForEach(BooruPostFileType.allCases, id: \.self) { type in - Text(type.rawValue.capitalized).tag(type) - } - } + var body: some View { + Picker("Thumbnail Type", selection: $settings.thumbnailType) { + ForEach(BooruPostFileType.allCases, id: \.self) { type in + Text(type.rawValue.capitalized).tag(type) + } + } - #if os(macOS) - Picker("Thumbnail Columns", selection: $settings.columns) { - ForEach(1 ... 10, id: \.self) { i in Text("\(i)") } - } - #else - Stepper( - "Thumbnail Columns: \(settings.columns)", - value: $settings.columns, - in: 1 ... 10 - ) - #endif + #if os(macOS) + Picker("Thumbnail Columns", selection: $settings.columns) { + ForEach(1...10, id: \.self) { i in Text("\(i)") } + } + #else + Stepper( + "Thumbnail Columns: \(settings.columns)", + value: $settings.columns, + in: 1...10 + ) + #endif - Toggle("Show NSFW Posts", isOn: $settings.showNSFWPosts) + Toggle("Show NSFW Posts", isOn: $settings.showNSFWPosts) - if settings.showNSFWPosts { - Toggle("Blur NSFW Thumbnails", isOn: $settings.blurNSFWThumbnails) - } + if settings.showNSFWPosts { + Toggle("Blur NSFW Thumbnails", isOn: $settings.blurNSFWThumbnails) } + } } diff --git a/Sora/Views/SettingsView.swift b/Sora/Views/SettingsView.swift index a4a37ae..ab92a42 100644 --- a/Sora/Views/SettingsView.swift +++ b/Sora/Views/SettingsView.swift @@ -1,44 +1,44 @@ import SwiftUI struct SettingsView: View { - @EnvironmentObject var settings: Settings - - var body: some View { - Form { - Section(header: Text("Provider")) { - SettingsProviderView() - } - - Section(header: Text("Thumbnails")) { - SettingsThumbnailsView() - } - - Section(header: Text("Details")) { - SettingsDetailsView() - } - - Section(header: Text("Search")) { - SettingsSearchView() - } - - Section(header: Text("Settings")) { - Button("Reset to Defaults") { - settings.resetToDefaults() - } - } - - Section(header: Text("Attributions")) { - SettingsAttributionsView() - } + @EnvironmentObject var settings: Settings + + var body: some View { + Form { + Section(header: Text("Provider")) { + SettingsProviderView() + } + + Section(header: Text("Thumbnails")) { + SettingsThumbnailsView() + } + + Section(header: Text("Details")) { + SettingsDetailsView() + } + + Section(header: Text("Search")) { + SettingsSearchView() + } + + Section(header: Text("Settings")) { + Button("Reset to Defaults") { + settings.resetToDefaults() } - #if os(macOS) - .formStyle(.grouped) - #endif - .navigationTitle("Settings") + } + + Section(header: Text("Attributions")) { + SettingsAttributionsView() + } } + #if os(macOS) + .formStyle(.grouped) + #endif + .navigationTitle("Settings") + } } #Preview { - SettingsView() - .environmentObject(Settings()) + SettingsView() + .environmentObject(Settings()) } |