summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-02-22 00:07:44 -0800
committerFuwn <[email protected]>2025-02-22 00:07:44 -0800
commite42fa12dafe264c665d2574c93b54ddafe7f2e1f (patch)
treea362b3b78ea97dc28ce5cc3682801bf89688f546
parentfeat: Development commit (diff)
downloadsora-testing-e42fa12dafe264c665d2574c93b54ddafe7f2e1f.tar.xz
sora-testing-e42fa12dafe264c665d2574c93b54ddafe7f2e1f.zip
feat: Development commit
-rw-r--r--Sora/Data/Booru/BooruManager.swift224
-rw-r--r--Sora/Data/Booru/BooruPost.swift46
-rw-r--r--Sora/Data/Booru/BooruPostFileType.swift6
-rw-r--r--Sora/Data/Booru/BooruPostXMLParser.swift208
-rw-r--r--Sora/Data/Booru/BooruProvider.swift18
-rw-r--r--Sora/Data/Booru/BooruTag.swift10
-rw-r--r--Sora/Data/Booru/BooruTagXMLParser.swift92
-rw-r--r--Sora/Data/Settings/Bookmark.swift20
-rw-r--r--Sora/Data/Settings/Settings.swift120
-rw-r--r--Sora/Other/AsyncImageWithPreview.swift294
-rw-r--r--Sora/Other/PostLoadingStage.swift6
-rw-r--r--Sora/SoraApp.swift48
-rw-r--r--Sora/Views/Bookmarks/BookmarkListItemView.swift56
-rw-r--r--Sora/Views/Bookmarks/BookmarksView.swift107
-rw-r--r--Sora/Views/ContentView.swift78
-rw-r--r--Sora/Views/MainView.swift104
-rw-r--r--Sora/Views/Post/PostDetailsView.swift142
-rw-r--r--Sora/Views/Post/PostGridBookmarkButtonView.swift49
-rw-r--r--Sora/Views/Post/PostGridView.swift171
-rw-r--r--Sora/Views/Post/PostView.swift67
-rw-r--r--Sora/Views/SearchSuggestionsView.swift32
-rw-r--r--Sora/Views/Settings/SettingsAttributionsView.swift10
-rw-r--r--Sora/Views/Settings/SettingsDetailsView.swift14
-rw-r--r--Sora/Views/Settings/SettingsProviderView.swift14
-rw-r--r--Sora/Views/Settings/SettingsSearchView.swift8
-rw-r--r--Sora/Views/Settings/SettingsThumbnailsView.swift44
-rw-r--r--Sora/Views/SettingsView.swift70
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())
}