summaryrefslogtreecommitdiff
path: root/Sora/Views
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-02-18 23:39:51 -0800
committerFuwn <[email protected]>2025-02-19 00:00:29 -0800
commit320ce31337ed60cae24a0374fa2d6d79237a6bfe (patch)
treead1e93799efeb8e7a16521e3ab958c911f73f617 /Sora/Views
parentfeat: Initial commit (diff)
downloadsora-testing-320ce31337ed60cae24a0374fa2d6d79237a6bfe.tar.xz
sora-testing-320ce31337ed60cae24a0374fa2d6d79237a6bfe.zip
feat: Development commit
Diffstat (limited to 'Sora/Views')
-rw-r--r--Sora/Views/ContentView.swift186
-rw-r--r--Sora/Views/MainView.swift27
-rw-r--r--Sora/Views/Post/PostDetailView.swift75
-rw-r--r--Sora/Views/Post/PostDetailsView.swift83
-rw-r--r--Sora/Views/Post/PostFileType.swift6
-rw-r--r--Sora/Views/Post/PostGridView.swift77
-rw-r--r--Sora/Views/Post/PostThumbnailMode.swift5
-rw-r--r--Sora/Views/Post/PostView.swift20
-rw-r--r--Sora/Views/SearchSuggestionsView.swift21
-rw-r--r--Sora/Views/Settings/SettingsDetailsView.swift31
-rw-r--r--Sora/Views/Settings/SettingsSearchView.swift22
-rw-r--r--Sora/Views/Settings/SettingsThumbnailsView.swift69
-rw-r--r--Sora/Views/SettingsView.swift32
-rw-r--r--Sora/Views/SettingsViewMacOS.swift30
14 files changed, 422 insertions, 262 deletions
diff --git a/Sora/Views/ContentView.swift b/Sora/Views/ContentView.swift
index 38516ef..9ec56a4 100644
--- a/Sora/Views/ContentView.swift
+++ b/Sora/Views/ContentView.swift
@@ -1,198 +1,32 @@
import SwiftUI
struct ContentView: View {
- @State private var posts: [MoebooruPost] = []
- #if os(macOS)
- @State private var selectedPost: YanderePost?
- #endif
- @State private var searchQuery = ""
- @State private var currentPage = 1
- @State private var isLoading: Bool = false
- @State private var largerThumbnails: Bool = false
- var tags: [String] {
- if searchQuery.isEmpty {
- return []
- }
-
- return searchQuery
- .split(separator: ",")
- .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
- .filter { !$0.isEmpty }
- }
-
- @State var softLimit: Int = 100
- var softLimitCGFloat: CGFloat {
- CGFloat(softLimit)
- }
-
- var columns: [GridItem] {
- return [
- GridItem(.adaptive(minimum: softLimitCGFloat)),
- ]
- }
+ @EnvironmentObject var settings: Settings
+ @StateObject private var manager = MoebooruManager()
var body: some View {
#if os(macOS)
NavigationSplitView {
- ScrollView {
- if self.posts.isEmpty {
- ProgressView()
- .frame(width: softLimitCGFloat, height: softLimitCGFloat)
- }
-
- LazyVGrid(columns: columns) {
- ForEach(posts, id: \.id) { post in
- Button {
- selectedPost = post
- } label: {
- PostView(post: post, softLimit: softLimitCGFloat)
- .frame(maxWidth: .infinity)
- }
- .buttonStyle(PlainButtonStyle())
-
- if post == posts.last {
- ProgressView()
- .onAppear(perform: loadNextPage)
- .padding()
- }
- }
- }
- }
- .searchable(text: $searchQuery, prompt: "Tags")
- .onSubmit(of: .search, performSearch)
- .toolbar {
- ToolbarItem {
- Button(action: {
- Task {
- await fetchPosts(page: 1, tags: tags, replace: true)
- }
- }) {
- Label("Refresh", systemImage: "arrow.clockwise")
- }
- }
- }
+ PostGridView(
+ manager: manager
+ )
} detail: {
- if let post = selectedPost {
- PostDetailView(post: post)
+ if let post = manager.selectedPost {
+ PostDetailsView(post: post)
} else {
- Text("Select a post")
+ Text("Select a post.")
.foregroundColor(.secondary)
}
}
- .task {
- await fetchPosts(page: currentPage)
- }
#else
NavigationStack {
- ScrollView {
- if self.posts.isEmpty {
- ProgressView()
- .frame(width: softLimitCGFloat, height: softLimitCGFloat)
- }
-
- LazyVGrid(columns: columns, spacing: 10) {
- ForEach(posts, id: \.id) { post in
- NavigationLink(value: post) {
- PostView(
- post: post,
- softLimit: softLimitCGFloat,
- thumbnailMode: self.largerThumbnails ? .sample : .preview
- )
- .frame(maxWidth: .infinity)
- }
-
- if post == posts.last {
- ProgressView()
- .onAppear(perform: loadNextPage)
- .padding()
- }
- }
- }
- }
- .searchable(text: $searchQuery, prompt: "Tags")
- .onSubmit(of: .search, performSearch)
- .navigationDestination(for: MoebooruPost.self) { post in
- PostDetailView(post: post)
- }
- .task {
- await fetchPosts(page: currentPage)
- }
- .toolbar {
- ToolbarItem {
- TextField(
- softLimit.description,
- value: $softLimit,
- format: .number
- )
- }
-
- ToolbarItem {
- Toggle("Full size thumbnails", isOn: $largerThumbnails)
- }
-
- ToolbarItem {
- Button(action: {
- Task {
- await fetchPosts(page: 1, tags: tags, replace: true)
- }
- }) {
- Label("Refresh", systemImage: "arrow.clockwise")
- }
- }
- }
+ PostGridView(manager: manager)
}
#endif
}
-
- func fetchPosts(page: Int = 1, limit: Int = 100, tags: [String] = [], replace: Bool = false) async {
- guard !isLoading else { return }
-
- isLoading = true
-
- defer { isLoading = false }
-
- guard let url = URL(string: "https://yande.re/post.xml?page=\(page)&limit=\(limit)&tags=\(tags.joined(separator: "+"))") else { return }
- do {
- let (data, _) = try await URLSession.shared.data(from: url)
-
- DispatchQueue.main.async {
- if replace {
- self.posts = []
- self.currentPage = 1
- }
-
- self.posts += Array(Set(MoebooruXMLParser().parse(data: data))).sorted { $0.id > $1.id }
- }
- } catch {
- #if DEBUG
- print(error)
- #endif
- }
- }
-
- func performSearch() {
- Task {
- await fetchPosts(
- page: 1,
- tags: tags,
- replace: true
- )
- }
- }
-
- func loadNextPage() {
- guard !isLoading else { return }
-
- Task {
- await fetchPosts(page: currentPage + 1, tags: tags)
-
- DispatchQueue.main.async {
- currentPage += 1
- }
- }
- }
}
#Preview {
ContentView()
+ .environmentObject(Settings())
}
diff --git a/Sora/Views/MainView.swift b/Sora/Views/MainView.swift
new file mode 100644
index 0000000..c657540
--- /dev/null
+++ b/Sora/Views/MainView.swift
@@ -0,0 +1,27 @@
+import SwiftUI
+
+struct MainView: View {
+ @EnvironmentObject var settings: Settings
+
+ var body: some View {
+ #if os(macOS)
+ ContentView()
+ .environmentObject(settings)
+ #else
+ TabView {
+ ContentView()
+ .tabItem {
+ Label("Posts", systemImage: "rectangle.stack")
+ }
+
+ NavigationStack {
+ SettingsView()
+ }
+ .tabItem {
+ Label("Settings", systemImage: "gear")
+ }
+ }
+ .environmentObject(settings)
+ #endif
+ }
+}
diff --git a/Sora/Views/Post/PostDetailView.swift b/Sora/Views/Post/PostDetailView.swift
deleted file mode 100644
index 799339c..0000000
--- a/Sora/Views/Post/PostDetailView.swift
+++ /dev/null
@@ -1,75 +0,0 @@
-import SwiftUI
-
-struct PostDetailView: View {
- let post: MoebooruPost
- @State var loadingStage: PostLoadingState = .loadingPreview
-
- var body: some View {
- VStack {
- Link(destination: URL(string: "https://yande.re/post/show/\(post.id)")!) {
- AsyncImage(url: post.sampleURL) { image in
- image
- .resizable()
- .scaledToFit()
- .onAppear {
- self.loadingStage = .loaded
- }
- } placeholder: {
- AsyncImage(url: post.previewURL) { image in
- image
- .resizable()
- .scaledToFit()
- .onAppear {
- self.loadingStage = .loadingFile
- }
- } placeholder: {
- ProgressView()
- .progressViewStyle(LinearProgressViewStyle())
- .padding()
- .onAppear {
- self.loadingStage = .loadingPreview
- }
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
- .id(post.fileURL)
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
- .id(post.fileURL)
- }
- .buttonStyle(PlainButtonStyle())
-
- Spacer()
-
- VStack(spacing: 5) {
- HStack {
- Text(post.tags.joined(separator: ", "))
- }
- .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 file …")
- case .loaded:
- EmptyView()
- }
- }
- .padding(.trailing, 5)
- }
- .frame(maxWidth: .infinity, alignment: .leading)
- .foregroundStyle(.secondary)
- }
- .padding(.horizontal, 10)
- .padding(.vertical, 10 / 1.33)
- .textSelection(.enabled)
- .font(.footnote)
- .background(.opacity(0.1))
- }
- }
-}
diff --git a/Sora/Views/Post/PostDetailsView.swift b/Sora/Views/Post/PostDetailsView.swift
new file mode 100644
index 0000000..8f8f1de
--- /dev/null
+++ b/Sora/Views/Post/PostDetailsView.swift
@@ -0,0 +1,83 @@
+import SwiftUI
+
+struct PostDetailsView: View {
+ @EnvironmentObject var settings: Settings
+ let post: MoebooruPost
+ @State var loadingStage: PostLoadingState = .loadingPreview
+ private var imageURL: URL? {
+ switch settings.detailViewType {
+ case .preview:
+ return post.previewURL
+ case .sample:
+ return post.sampleURL
+ case .original:
+ return post.fileURL
+ case .compressed:
+ return post.jpegURL
+ }
+ }
+
+ 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)
+
+ 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)
+ }
+ .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/PostFileType.swift b/Sora/Views/Post/PostFileType.swift
new file mode 100644
index 0000000..a7b3ca6
--- /dev/null
+++ b/Sora/Views/Post/PostFileType.swift
@@ -0,0 +1,6 @@
+enum PostFileType: String, CaseIterable {
+ case original
+ case sample
+ case compressed
+ case preview
+}
diff --git a/Sora/Views/Post/PostGridView.swift b/Sora/Views/Post/PostGridView.swift
new file mode 100644
index 0000000..4c43eeb
--- /dev/null
+++ b/Sora/Views/Post/PostGridView.swift
@@ -0,0 +1,77 @@
+import SwiftUI
+import WaterfallGrid
+
+struct PostGridView: View {
+ @EnvironmentObject var settings: Settings
+ @ObservedObject var manager: MoebooruManager
+
+ var body: some View {
+ ScrollViewReader { _ in
+ ScrollView {
+ if manager.posts.isEmpty {
+ ProgressView()
+ .frame(
+ width: settings.softLimitAsCGFloat(),
+ height: settings.softLimitAsCGFloat()
+ )
+ }
+
+ WaterfallGrid(manager.posts, id: \.id) { post in
+ Group {
+ #if os(macOS)
+ Button {
+ manager.selectedPost = post
+ } label: {
+ PostView(post: post, manager: manager)
+ }
+ .buttonStyle(PlainButtonStyle())
+ #else
+ NavigationLink(value: post) {
+ PostView(post: post, manager: manager)
+ }
+ #endif
+ }
+ }
+ .gridStyle(columns: settings.columns, spacing: 0)
+ }
+ .searchable(text: $manager.searchText, prompt: "Tags")
+ .searchSuggestions {
+ if settings.searchSuggestions {
+ SearchSuggestionsView(
+ tags: manager.allTags,
+ searchText: $manager.searchText
+ )
+ }
+ }
+ .onSubmit(of: .search, manager.performSearch)
+ .task {
+ if manager.posts.isEmpty {
+ await manager.fetchPosts(page: manager.currentPage)
+ }
+ }
+ .navigationDestination(for: MoebooruPost.self) { post in
+ PostDetailsView(post: post)
+ }
+ #if os(macOS)
+ .toolbar {
+ ToolbarItem {
+ Button(action: {
+ Task {
+ await manager.fetchPosts(page: 1, tags: manager.tags, replace: true)
+ }
+ }) {
+ Label("Refresh", systemImage: "arrow.clockwise")
+ }
+ }
+ }
+ #endif
+ .navigationTitle("Posts")
+ .refreshable {
+ await manager.fetchPosts(page: 1, tags: manager.tags, replace: true)
+ }
+ #if os(iOS)
+ .navigationBarTitleDisplayMode(.inline)
+ #endif
+ }
+ }
+}
diff --git a/Sora/Views/Post/PostThumbnailMode.swift b/Sora/Views/Post/PostThumbnailMode.swift
deleted file mode 100644
index 1e6c3b3..0000000
--- a/Sora/Views/Post/PostThumbnailMode.swift
+++ /dev/null
@@ -1,5 +0,0 @@
-enum PostThumbnailMode {
- case preview
- case sample
- case file
-}
diff --git a/Sora/Views/Post/PostView.swift b/Sora/Views/Post/PostView.swift
index 97e311c..d070aac 100644
--- a/Sora/Views/Post/PostView.swift
+++ b/Sora/Views/Post/PostView.swift
@@ -1,17 +1,19 @@
import SwiftUI
struct PostView: View {
+ @EnvironmentObject var settings: Settings
let post: MoebooruPost
- var softLimit: CGFloat
- var thumbnailMode: PostThumbnailMode = .preview
+ @ObservedObject var manager: MoebooruManager
private var thumbnailURL: URL? {
- switch thumbnailMode {
+ switch settings.thumbnailType {
case .preview:
return post.previewURL
case .sample:
return post.sampleURL
- case .file:
+ case .original:
return post.fileURL
+ case .compressed:
+ return post.jpegURL
}
}
@@ -20,11 +22,17 @@ struct PostView: View {
AsyncImage(url: thumbnailURL) { image in
image
.resizable()
- .scaledToFit()
+ .aspectRatio(contentMode: .fit)
+ .onScrollVisibilityChange { visible in
+ if post == manager.posts.last && !manager.endOfData && visible {
+ Task {
+ manager.loadNextPage()
+ }
+ }
+ }
} placeholder: {
ProgressView()
}
- .frame(width: softLimit, height: softLimit)
}
}
}
diff --git a/Sora/Views/SearchSuggestionsView.swift b/Sora/Views/SearchSuggestionsView.swift
new file mode 100644
index 0000000..cae8d43
--- /dev/null
+++ b/Sora/Views/SearchSuggestionsView.swift
@@ -0,0 +1,21 @@
+import SwiftUI
+
+struct SearchSuggestionsView: View {
+ var tags: [MoebooruTag]
+ @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)
+ }
+ }
+ }
+}
diff --git a/Sora/Views/Settings/SettingsDetailsView.swift b/Sora/Views/Settings/SettingsDetailsView.swift
new file mode 100644
index 0000000..29b9e76
--- /dev/null
+++ b/Sora/Views/Settings/SettingsDetailsView.swift
@@ -0,0 +1,31 @@
+import SwiftUI
+
+struct SettingsDetailsView: View {
+ @EnvironmentObject var settings: Settings
+
+ var body: some View {
+ #if os(macOS)
+ Text("Details")
+ .font(.headline)
+
+ HStack {
+ Text("Detail View Type")
+
+ Spacer()
+
+ Picker("", selection: $settings.detailViewType) {
+ ForEach(PostFileType.allCases, id: \.self) { type in
+ Text(type.rawValue.capitalized).tag(type)
+ }
+ }
+ .frame(width: 150)
+ }
+ #else
+ Picker("Detail View Type", selection: $settings.detailViewType) {
+ ForEach(PostFileType.allCases, id: \.self) { type in
+ Text(type.rawValue.capitalized).tag(type)
+ }
+ }
+ #endif
+ }
+}
diff --git a/Sora/Views/Settings/SettingsSearchView.swift b/Sora/Views/Settings/SettingsSearchView.swift
new file mode 100644
index 0000000..c1f1baa
--- /dev/null
+++ b/Sora/Views/Settings/SettingsSearchView.swift
@@ -0,0 +1,22 @@
+import SwiftUI
+
+struct SettingsSearchView: View {
+ @EnvironmentObject var settings: Settings
+
+ var body: some View {
+ #if os(macOS)
+ Text("Search")
+ .font(.headline)
+
+ HStack {
+ Text("Suggest Search Tags")
+
+ Spacer()
+
+ Toggle("", isOn: $settings.searchSuggestions)
+ }
+ #else
+ Toggle("Suggest Search Tags", isOn: $settings.searchSuggestions)
+ #endif
+ }
+}
diff --git a/Sora/Views/Settings/SettingsThumbnailsView.swift b/Sora/Views/Settings/SettingsThumbnailsView.swift
new file mode 100644
index 0000000..552ef0c
--- /dev/null
+++ b/Sora/Views/Settings/SettingsThumbnailsView.swift
@@ -0,0 +1,69 @@
+import SwiftUI
+
+struct SettingsThumbnailsView: View {
+ @EnvironmentObject var settings: Settings
+
+ var body: some View {
+ #if os(macOS)
+ Text("Thumbnails")
+ .font(.headline)
+
+ HStack {
+ Text("Thumbnail Size")
+
+ Spacer()
+
+ TextField("", value: $settings.softLimit, format: .number)
+ .textFieldStyle(.roundedBorder)
+ .frame(width: 100)
+ }
+ #else
+ Stepper(
+ "Thumbnail Size (\(settings.softLimit))",
+ value: $settings.softLimit,
+ in: 100 ... 10000,
+ step: 10
+ )
+ #endif
+
+ #if os(macOS)
+ HStack {
+ Text("Thumbnail Type")
+
+ Spacer()
+
+ Picker("", selection: $settings.thumbnailType) {
+ ForEach(PostFileType.allCases, id: \.self) { type in
+ Text(type.rawValue.capitalized).tag(type)
+ }
+ }
+ .frame(width: 150)
+ }
+ #else
+ Picker("Thumbnail Type", selection: $settings.thumbnailType) {
+ ForEach(PostFileType.allCases, id: \.self) { type in
+ Text(type.rawValue.capitalized).tag(type)
+ }
+ }
+ #endif
+
+ #if os(macOS)
+ HStack {
+ Text("Thumbnail Columns")
+
+ Spacer()
+
+ Picker("", selection: $settings.columns) {
+ ForEach(1 ... 10, id: \.self) { i in Text("\(i)") }
+ }
+ .frame(width: 75)
+ }
+ #else
+ Stepper(
+ "Thumbnail Columns (\(settings.columns))",
+ value: $settings.columns,
+ in: 1 ... 10
+ )
+ #endif
+ }
+}
diff --git a/Sora/Views/SettingsView.swift b/Sora/Views/SettingsView.swift
new file mode 100644
index 0000000..105b5b6
--- /dev/null
+++ b/Sora/Views/SettingsView.swift
@@ -0,0 +1,32 @@
+import SwiftUI
+
+struct SettingsView: View {
+ @EnvironmentObject var settings: Settings
+
+ var body: some View {
+ NavigationStack {
+ Form {
+ Section(header: Text("Thumbnails")) {
+ SettingsThumbnailsView()
+ }
+
+ Section(header: Text("Details")) {
+ SettingsDetailsView()
+ }
+
+ Section(header: Text("Search")) {
+ SettingsSearchView()
+ }
+ }
+ }
+ .navigationTitle("Settings")
+ #if os(macOS)
+ .padding()
+ #endif
+ }
+}
+
+#Preview {
+ SettingsView()
+ .environmentObject(Settings())
+}
diff --git a/Sora/Views/SettingsViewMacOS.swift b/Sora/Views/SettingsViewMacOS.swift
new file mode 100644
index 0000000..a981a59
--- /dev/null
+++ b/Sora/Views/SettingsViewMacOS.swift
@@ -0,0 +1,30 @@
+import SwiftUI
+
+struct SettingsViewMacOS: View {
+ @EnvironmentObject var settings: Settings
+ @State private var selection: Int = 0
+
+ var body: some View {
+ NavigationStack {
+ VStack(alignment: .leading) {
+ SettingsThumbnailsView()
+
+ Spacer()
+ .frame(height: 25)
+
+ SettingsDetailsView()
+
+ Spacer()
+ .frame(height: 25)
+
+ SettingsSearchView()
+ }
+ .padding()
+ }
+ }
+}
+
+#Preview {
+ SettingsViewMacOS()
+ .environmentObject(Settings())
+}