From cafece91bae45194d64f4932bb04be018b82d21b Mon Sep 17 00:00:00 2001 From: Fuwn Date: Sat, 22 Feb 2025 07:07:57 -0800 Subject: feat: Development commit --- Sora/Data/Booru/BooruPostLoadingState.swift | 5 + Sora/Other/AsyncImageWithPreview.swift | 106 --------------------- Sora/Other/PostLoadingState.swift | 5 - Sora/Views/Bookmarks/BookmarkListItemView.swift | 39 -------- Sora/Views/Bookmarks/BookmarksListItemView.swift | 39 ++++++++ Sora/Views/Bookmarks/BookmarksView.swift | 2 +- Sora/Views/InteractiveImageView.swift | 89 +++++++++++++++++ Sora/Views/Post/Details/PostDetailsImageView.swift | 106 +++++++++++++++++++++ Sora/Views/Post/Details/PostDetailsView.swift | 87 +++++++++++++++++ .../Post/Grid/PostGridBookmarkButtonView.swift | 37 +++++++ Sora/Views/Post/Grid/PostGridThumbnailView.swift | 47 +++++++++ Sora/Views/Post/Grid/PostGridView.swift | 101 ++++++++++++++++++++ Sora/Views/Post/PostDetailsView.swift | 87 ----------------- Sora/Views/Post/PostGridBookmarkButtonView.swift | 37 ------- Sora/Views/Post/PostGridView.swift | 101 -------------------- Sora/Views/Post/PostView.swift | 47 --------- .../Section/SettingsAttributionsView.swift | 9 ++ .../Settings/Section/SettingsDetailsView.swift | 15 +++ .../Settings/Section/SettingsProviderView.swift | 13 +++ .../Settings/Section/SettingsSearchView.swift | 9 ++ .../Settings/Section/SettingsThumbnailsView.swift | 31 ++++++ Sora/Views/Settings/SettingsAttributionsView.swift | 9 -- Sora/Views/Settings/SettingsDetailsView.swift | 15 --- Sora/Views/Settings/SettingsProviderView.swift | 13 --- Sora/Views/Settings/SettingsSearchView.swift | 9 -- Sora/Views/Settings/SettingsThumbnailsView.swift | 31 ------ Sora/Views/Settings/SettingsView.swift | 44 +++++++++ Sora/Views/SettingsView.swift | 44 --------- Sora/Views/ZoomableImageView.swift | 89 ----------------- 29 files changed, 633 insertions(+), 633 deletions(-) create mode 100644 Sora/Data/Booru/BooruPostLoadingState.swift delete mode 100644 Sora/Other/AsyncImageWithPreview.swift delete mode 100644 Sora/Other/PostLoadingState.swift delete mode 100644 Sora/Views/Bookmarks/BookmarkListItemView.swift create mode 100644 Sora/Views/Bookmarks/BookmarksListItemView.swift create mode 100644 Sora/Views/InteractiveImageView.swift create mode 100644 Sora/Views/Post/Details/PostDetailsImageView.swift create mode 100644 Sora/Views/Post/Details/PostDetailsView.swift create mode 100644 Sora/Views/Post/Grid/PostGridBookmarkButtonView.swift create mode 100644 Sora/Views/Post/Grid/PostGridThumbnailView.swift create mode 100644 Sora/Views/Post/Grid/PostGridView.swift delete mode 100644 Sora/Views/Post/PostDetailsView.swift delete mode 100644 Sora/Views/Post/PostGridBookmarkButtonView.swift delete mode 100644 Sora/Views/Post/PostGridView.swift delete mode 100644 Sora/Views/Post/PostView.swift create mode 100644 Sora/Views/Settings/Section/SettingsAttributionsView.swift create mode 100644 Sora/Views/Settings/Section/SettingsDetailsView.swift create mode 100644 Sora/Views/Settings/Section/SettingsProviderView.swift create mode 100644 Sora/Views/Settings/Section/SettingsSearchView.swift create mode 100644 Sora/Views/Settings/Section/SettingsThumbnailsView.swift delete mode 100644 Sora/Views/Settings/SettingsAttributionsView.swift delete mode 100644 Sora/Views/Settings/SettingsDetailsView.swift delete mode 100644 Sora/Views/Settings/SettingsProviderView.swift delete mode 100644 Sora/Views/Settings/SettingsSearchView.swift delete mode 100644 Sora/Views/Settings/SettingsThumbnailsView.swift create mode 100644 Sora/Views/Settings/SettingsView.swift delete mode 100644 Sora/Views/SettingsView.swift delete mode 100644 Sora/Views/ZoomableImageView.swift diff --git a/Sora/Data/Booru/BooruPostLoadingState.swift b/Sora/Data/Booru/BooruPostLoadingState.swift new file mode 100644 index 0000000..c413ee4 --- /dev/null +++ b/Sora/Data/Booru/BooruPostLoadingState.swift @@ -0,0 +1,5 @@ +enum BooruPostLoadingState { + case loaded + case loadingFile + case loadingPreview +} diff --git a/Sora/Other/AsyncImageWithPreview.swift b/Sora/Other/AsyncImageWithPreview.swift deleted file mode 100644 index 56af307..0000000 --- a/Sora/Other/AsyncImageWithPreview.swift +++ /dev/null @@ -1,106 +0,0 @@ -import SwiftUI - -struct AsyncImageWithPreview: View { - @EnvironmentObject var settings: Settings - 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 - - #if os(iOS) - var keyWindow: UIWindow? { - guard - let window = UIApplication.shared.connectedScenes - .compactMap({ $0 as? UIWindowScene }) - .flatMap(\.windows) - .first(where: \.isKeyWindow) - else { - return nil - } - - return window - } - #endif - - var body: some View { - AsyncImage(url: url) { image in - ZoomableImageView(image: image) - .onAppear { - loadingState = finalLoadingState - } - .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 } - - UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil) - } - .resume() - } label: { - Label("Save Image", systemImage: "square.and.arrow.down") - } - #endif - - #if os(iOS) - if settings.enableShareShortcut { - Button { - keyWindow?.rootViewController?.present( - UIActivityViewController( - activityItems: [url ?? URL(string: "")!], applicationActivities: nil - ), 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 - } - } - } - - init( - url: URL?, - loadingStage: Binding, - 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 - } -} diff --git a/Sora/Other/PostLoadingState.swift b/Sora/Other/PostLoadingState.swift deleted file mode 100644 index 7af533f..0000000 --- a/Sora/Other/PostLoadingState.swift +++ /dev/null @@ -1,5 +0,0 @@ -enum PostLoadingState { - case loaded - case loadingFile - case loadingPreview -} diff --git a/Sora/Views/Bookmarks/BookmarkListItemView.swift b/Sora/Views/Bookmarks/BookmarkListItemView.swift deleted file mode 100644 index 611c9fe..0000000 --- a/Sora/Views/Bookmarks/BookmarkListItemView.swift +++ /dev/null @@ -1,39 +0,0 @@ -import SwiftUI - -struct BookmarkListItemView: View { - @EnvironmentObject var settings: Settings - var bookmark: Bookmark - - var body: some View { - VStack(alignment: .leading) { - HStack { - Text(bookmark.tags.joined(separator: ", ")) - - #if os(macOS) - Spacer() - - Button { - settings.removeBookmark(withID: bookmark.id) - } label: { - Image(systemName: "trash") - } - #endif - } - - HStack { - Text(bookmark.createdAt, style: .date) - .font(.caption) - .foregroundStyle(Color.secondary) - - Spacer() - - Text(bookmark.provider.rawValue) - .font(.caption) - .foregroundStyle(Color.secondary) - } - } - #if os(macOS) - .padding() - #endif - } -} diff --git a/Sora/Views/Bookmarks/BookmarksListItemView.swift b/Sora/Views/Bookmarks/BookmarksListItemView.swift new file mode 100644 index 0000000..cfe267b --- /dev/null +++ b/Sora/Views/Bookmarks/BookmarksListItemView.swift @@ -0,0 +1,39 @@ +import SwiftUI + +struct BookmarksListItemView: View { + @EnvironmentObject var settings: Settings + var bookmark: Bookmark + + var body: some View { + VStack(alignment: .leading) { + HStack { + Text(bookmark.tags.joined(separator: ", ")) + + #if os(macOS) + Spacer() + + Button { + settings.removeBookmark(withID: bookmark.id) + } label: { + Image(systemName: "trash") + } + #endif + } + + HStack { + Text(bookmark.createdAt, style: .date) + .font(.caption) + .foregroundStyle(Color.secondary) + + Spacer() + + Text(bookmark.provider.rawValue) + .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 b52e7b7..b32b46b 100644 --- a/Sora/Views/Bookmarks/BookmarksView.swift +++ b/Sora/Views/Bookmarks/BookmarksView.swift @@ -47,7 +47,7 @@ struct BookmarksView: View { manager.performSearch() } }) { - BookmarkListItemView(bookmark: bookmark) + BookmarksListItemView(bookmark: bookmark) } #if os(macOS) .buttonStyle(.plain) diff --git a/Sora/Views/InteractiveImageView.swift b/Sora/Views/InteractiveImageView.swift new file mode 100644 index 0000000..7afde7d --- /dev/null +++ b/Sora/Views/InteractiveImageView.swift @@ -0,0 +1,89 @@ +import SwiftUI + +struct InteractiveImageView: View { + let image: Image + @State private var screenWidth = 0.0 + @State private var screenHeight = 0.0 + @State private var currentScale = 1.0 + @State private var previousScale = 0.0 + @State private var currentOffset: CGSize = .zero + @State private var previousOffset: CGSize = .zero + + var body: some View { + GeometryReader { geometry in + VStack { + image + .resizable() + .scaledToFit() + .scaleEffect(currentScale) + .offset(currentOffset) + .frame(width: screenWidth, height: screenHeight) + .clipped() + .gesture( + MagnifyGesture() + .onChanged { gesture in + withAnimation(.interactiveSpring()) { + currentScale = + previousScale + gesture.magnification - (previousScale == 0 ? 0 : 1) + } + } + .onEnded { _ in + previousScale = currentScale + } + .simultaneously( + with: DragGesture(minimumDistance: 0) + .onChanged { gesture in + withAnimation(.interactiveSpring()) { + var newOffset: CGSize = .zero + let offset = gesture.translation + + newOffset.width = offset.width + previousOffset.width + newOffset.height = offset.height + previousOffset.height + + currentOffset = clampOffset(offset: newOffset) + } + } + .onEnded { _ in + previousOffset = currentOffset + } + ) + ) + .highPriorityGesture( + TapGesture(count: 2) + .onEnded { + withAnimation { + currentScale = 1.0 + previousScale = 0 + currentOffset = .zero + previousOffset = .zero + } + } + ) + } + .onAppear { + screenWidth = geometry.size.width + screenHeight = geometry.size.height + } + } + } + + private func clampOffset(offset: CGSize = .zero) -> CGSize { + var newOffset = offset + + if currentScale > 1 { + let maxX = ((screenWidth * currentScale) - screenWidth) / 2 + let maxY = ((screenHeight * currentScale) - screenHeight) / 2 + + newOffset.width = min(max(-maxX, newOffset.width), maxX) + newOffset.height = min(max(-maxY, newOffset.height), maxY) + } else { + newOffset = .zero + } + + return newOffset + } +} + +#Preview { + InteractiveImageView(image: Image(systemName: "photo")) +} diff --git a/Sora/Views/Post/Details/PostDetailsImageView.swift b/Sora/Views/Post/Details/PostDetailsImageView.swift new file mode 100644 index 0000000..d6307d5 --- /dev/null +++ b/Sora/Views/Post/Details/PostDetailsImageView.swift @@ -0,0 +1,106 @@ +import SwiftUI + +struct PostDetailsImageView: View { + @EnvironmentObject var settings: Settings + var url: URL? + @Binding var loadingState: BooruPostLoadingState + var finalLoadingState: BooruPostLoadingState + 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 + + #if os(iOS) + var keyWindow: UIWindow? { + guard + let window = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .flatMap(\.windows) + .first(where: \.isKeyWindow) + else { + return nil + } + + return window + } + #endif + + var body: some View { + AsyncImage(url: url) { image in + InteractiveImageView(image: image) + .onAppear { + loadingState = finalLoadingState + } + .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 } + + UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil) + } + .resume() + } label: { + Label("Save Image", systemImage: "square.and.arrow.down") + } + #endif + + #if os(iOS) + if settings.enableShareShortcut { + Button { + keyWindow?.rootViewController?.present( + UIActivityViewController( + activityItems: [url ?? URL(string: "")!], applicationActivities: nil + ), 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 + } + } + } + + init( + url: URL?, + loadingStage: Binding, + finalLoadingState: BooruPostLoadingState = .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 + } +} diff --git a/Sora/Views/Post/Details/PostDetailsView.swift b/Sora/Views/Post/Details/PostDetailsView.swift new file mode 100644 index 0000000..3c004ca --- /dev/null +++ b/Sora/Views/Post/Details/PostDetailsView.swift @@ -0,0 +1,87 @@ +import SwiftUI + +struct PostDetailsView: View { + @EnvironmentObject var settings: Settings + let post: BooruPost + @State private var loadingStage: BooruPostLoadingState = .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) { + PostDetailsImageView( + url: imageURL, + loadingStage: $loadingStage, + finalLoadingState: .loaded, + postURL: URL(string: "https://yande.re/post/show/\(post.id)")! + ) { + PostDetailsImageView( + 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/Grid/PostGridBookmarkButtonView.swift b/Sora/Views/Post/Grid/PostGridBookmarkButtonView.swift new file mode 100644 index 0000000..9f85a20 --- /dev/null +++ b/Sora/Views/Post/Grid/PostGridBookmarkButtonView.swift @@ -0,0 +1,37 @@ +import SwiftUI + +struct PostGridBookmarkButtonView: View { + @EnvironmentObject private var manager: BooruManager + @EnvironmentObject private var settings: Settings + + var contained: Bool { + let lowercaseTags = manager.tags.map { $0.lowercased() } + + return settings.bookmarks + .contains { bookmark in + bookmark.tags == lowercaseTags + && bookmark.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" + ) + } + } +} diff --git a/Sora/Views/Post/Grid/PostGridThumbnailView.swift b/Sora/Views/Post/Grid/PostGridThumbnailView.swift new file mode 100644 index 0000000..e90b39b --- /dev/null +++ b/Sora/Views/Post/Grid/PostGridThumbnailView.swift @@ -0,0 +1,47 @@ +import SwiftUI + +struct PostGridThumbnailView: View { + @EnvironmentObject var settings: Settings + @EnvironmentObject var manager: BooruManager + let post: BooruPost + 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 + ) + .clipped() + .animation(.default, value: settings.blurNSFWThumbnails) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } placeholder: { + ProgressView() + .padding() + } + } + } +} diff --git a/Sora/Views/Post/Grid/PostGridView.swift b/Sora/Views/Post/Grid/PostGridView.swift new file mode 100644 index 0000000..9738a15 --- /dev/null +++ b/Sora/Views/Post/Grid/PostGridView.swift @@ -0,0 +1,101 @@ +import SwiftUI +import WaterfallGrid + +struct PostGridView: View { + @EnvironmentObject var settings: Settings + @EnvironmentObject 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 { $0.id > $1.id } + } + + 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: { + PostGridThumbnailView( + post: post, + posts: filteredPosts + ) + } + .buttonStyle(PlainButtonStyle()) + #else + NavigationLink(value: post) { + PostGridThumbnailView( + post: post, + posts: filteredPosts + ) + } + #endif + } + } + .gridStyle(columns: settings.columns) + .padding(8) + } + .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") + } + } + #endif + + if !manager.tags.isEmpty { + #if os(macOS) + ToolbarItem { + PostGridBookmarkButtonView() + } + #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/PostDetailsView.swift b/Sora/Views/Post/PostDetailsView.swift deleted file mode 100644 index f61b82a..0000000 --- a/Sora/Views/Post/PostDetailsView.swift +++ /dev/null @@ -1,87 +0,0 @@ -import SwiftUI - -struct PostDetailsView: View { - @EnvironmentObject var settings: Settings - let post: BooruPost - @State private 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) - - 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/PostGridBookmarkButtonView.swift b/Sora/Views/Post/PostGridBookmarkButtonView.swift deleted file mode 100644 index 9f85a20..0000000 --- a/Sora/Views/Post/PostGridBookmarkButtonView.swift +++ /dev/null @@ -1,37 +0,0 @@ -import SwiftUI - -struct PostGridBookmarkButtonView: View { - @EnvironmentObject private var manager: BooruManager - @EnvironmentObject private var settings: Settings - - var contained: Bool { - let lowercaseTags = manager.tags.map { $0.lowercased() } - - return settings.bookmarks - .contains { bookmark in - bookmark.tags == lowercaseTags - && bookmark.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" - ) - } - } -} diff --git a/Sora/Views/Post/PostGridView.swift b/Sora/Views/Post/PostGridView.swift deleted file mode 100644 index fb813af..0000000 --- a/Sora/Views/Post/PostGridView.swift +++ /dev/null @@ -1,101 +0,0 @@ -import SwiftUI -import WaterfallGrid - -struct PostGridView: View { - @EnvironmentObject var settings: Settings - @EnvironmentObject 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 { $0.id > $1.id } - } - - 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, - posts: filteredPosts - ) - } - .buttonStyle(PlainButtonStyle()) - #else - NavigationLink(value: post) { - PostView( - post: post, - posts: filteredPosts - ) - } - #endif - } - } - .gridStyle(columns: settings.columns) - .padding(8) - } - .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") - } - } - #endif - - if !manager.tags.isEmpty { - #if os(macOS) - ToolbarItem { - PostGridBookmarkButtonView() - } - #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 deleted file mode 100644 index c2cafb2..0000000 --- a/Sora/Views/Post/PostView.swift +++ /dev/null @@ -1,47 +0,0 @@ -import SwiftUI - -struct PostView: View { - @EnvironmentObject var settings: Settings - @EnvironmentObject var manager: BooruManager - let post: BooruPost - 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 - ) - .clipped() - .animation(.default, value: settings.blurNSFWThumbnails) - .clipShape(RoundedRectangle(cornerRadius: 8)) - } placeholder: { - ProgressView() - .padding() - } - } - } -} diff --git a/Sora/Views/Settings/Section/SettingsAttributionsView.swift b/Sora/Views/Settings/Section/SettingsAttributionsView.swift new file mode 100644 index 0000000..3c94c4e --- /dev/null +++ b/Sora/Views/Settings/Section/SettingsAttributionsView.swift @@ -0,0 +1,9 @@ +import SwiftUI + +struct SettingsAttributionsView: View { + var body: some View { + Text("Rabbit SVG created by Kim Sun Young") + .fontWeight(.light) + .foregroundColor(.secondary) + } +} diff --git a/Sora/Views/Settings/Section/SettingsDetailsView.swift b/Sora/Views/Settings/Section/SettingsDetailsView.swift new file mode 100644 index 0000000..c51ab76 --- /dev/null +++ b/Sora/Views/Settings/Section/SettingsDetailsView.swift @@ -0,0 +1,15 @@ +import SwiftUI + +struct SettingsDetailsView: View { + @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) + } + } + + Toggle("Enable \"Share Image\" Shortcut", isOn: $settings.enableShareShortcut) + } +} diff --git a/Sora/Views/Settings/Section/SettingsProviderView.swift b/Sora/Views/Settings/Section/SettingsProviderView.swift new file mode 100644 index 0000000..1de25c3 --- /dev/null +++ b/Sora/Views/Settings/Section/SettingsProviderView.swift @@ -0,0 +1,13 @@ +import SwiftUI + +struct SettingsProviderView: View { + @EnvironmentObject var settings: Settings + + var body: some View { + Picker("Provider", selection: $settings.preferredBooru) { + ForEach(BooruProvider.allCases, id: \.self) { type in + Text(type.rawValue).tag(type) + } + } + } +} diff --git a/Sora/Views/Settings/Section/SettingsSearchView.swift b/Sora/Views/Settings/Section/SettingsSearchView.swift new file mode 100644 index 0000000..63be2f1 --- /dev/null +++ b/Sora/Views/Settings/Section/SettingsSearchView.swift @@ -0,0 +1,9 @@ +import SwiftUI + +struct SettingsSearchView: View { + @EnvironmentObject var settings: Settings + + var body: some View { + Toggle("Suggest Search Tags", isOn: $settings.searchSuggestions) + } +} diff --git a/Sora/Views/Settings/Section/SettingsThumbnailsView.swift b/Sora/Views/Settings/Section/SettingsThumbnailsView.swift new file mode 100644 index 0000000..f787e59 --- /dev/null +++ b/Sora/Views/Settings/Section/SettingsThumbnailsView.swift @@ -0,0 +1,31 @@ +import SwiftUI + +struct SettingsThumbnailsView: View { + @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) + } + } + + #if os(macOS) + Picker("Thumbnail Columns", selection: $settings.columns) { + ForEach(1...10, id: \.self) { columns in Text("\(columns)") } + } + #else + Stepper( + "Thumbnail Columns: \(settings.columns)", + value: $settings.columns, + in: 1...10 + ) + #endif + + Toggle("Show NSFW Posts", isOn: $settings.showNSFWPosts) + + if settings.showNSFWPosts { + Toggle("Blur NSFW Thumbnails", isOn: $settings.blurNSFWThumbnails) + } + } +} diff --git a/Sora/Views/Settings/SettingsAttributionsView.swift b/Sora/Views/Settings/SettingsAttributionsView.swift deleted file mode 100644 index 3c94c4e..0000000 --- a/Sora/Views/Settings/SettingsAttributionsView.swift +++ /dev/null @@ -1,9 +0,0 @@ -import SwiftUI - -struct SettingsAttributionsView: View { - 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 deleted file mode 100644 index c51ab76..0000000 --- a/Sora/Views/Settings/SettingsDetailsView.swift +++ /dev/null @@ -1,15 +0,0 @@ -import SwiftUI - -struct SettingsDetailsView: View { - @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) - } - } - - Toggle("Enable \"Share Image\" Shortcut", isOn: $settings.enableShareShortcut) - } -} diff --git a/Sora/Views/Settings/SettingsProviderView.swift b/Sora/Views/Settings/SettingsProviderView.swift deleted file mode 100644 index 1de25c3..0000000 --- a/Sora/Views/Settings/SettingsProviderView.swift +++ /dev/null @@ -1,13 +0,0 @@ -import SwiftUI - -struct SettingsProviderView: View { - @EnvironmentObject var settings: Settings - - var body: some View { - Picker("Provider", selection: $settings.preferredBooru) { - ForEach(BooruProvider.allCases, id: \.self) { type in - Text(type.rawValue).tag(type) - } - } - } -} diff --git a/Sora/Views/Settings/SettingsSearchView.swift b/Sora/Views/Settings/SettingsSearchView.swift deleted file mode 100644 index 63be2f1..0000000 --- a/Sora/Views/Settings/SettingsSearchView.swift +++ /dev/null @@ -1,9 +0,0 @@ -import SwiftUI - -struct SettingsSearchView: View { - @EnvironmentObject var settings: Settings - - 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 deleted file mode 100644 index f787e59..0000000 --- a/Sora/Views/Settings/SettingsThumbnailsView.swift +++ /dev/null @@ -1,31 +0,0 @@ -import SwiftUI - -struct SettingsThumbnailsView: View { - @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) - } - } - - #if os(macOS) - Picker("Thumbnail Columns", selection: $settings.columns) { - ForEach(1...10, id: \.self) { columns in Text("\(columns)") } - } - #else - Stepper( - "Thumbnail Columns: \(settings.columns)", - value: $settings.columns, - in: 1...10 - ) - #endif - - Toggle("Show NSFW Posts", isOn: $settings.showNSFWPosts) - - if settings.showNSFWPosts { - Toggle("Blur NSFW Thumbnails", isOn: $settings.blurNSFWThumbnails) - } - } -} diff --git a/Sora/Views/Settings/SettingsView.swift b/Sora/Views/Settings/SettingsView.swift new file mode 100644 index 0000000..ab92a42 --- /dev/null +++ b/Sora/Views/Settings/SettingsView.swift @@ -0,0 +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() + } + } + #if os(macOS) + .formStyle(.grouped) + #endif + .navigationTitle("Settings") + } +} + +#Preview { + SettingsView() + .environmentObject(Settings()) +} diff --git a/Sora/Views/SettingsView.swift b/Sora/Views/SettingsView.swift deleted file mode 100644 index ab92a42..0000000 --- a/Sora/Views/SettingsView.swift +++ /dev/null @@ -1,44 +0,0 @@ -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() - } - } - #if os(macOS) - .formStyle(.grouped) - #endif - .navigationTitle("Settings") - } -} - -#Preview { - SettingsView() - .environmentObject(Settings()) -} diff --git a/Sora/Views/ZoomableImageView.swift b/Sora/Views/ZoomableImageView.swift deleted file mode 100644 index 8304c6e..0000000 --- a/Sora/Views/ZoomableImageView.swift +++ /dev/null @@ -1,89 +0,0 @@ -import SwiftUI - -struct ZoomableImageView: View { - let image: Image - @State private var screenWidth = 0.0 - @State private var screenHeight = 0.0 - @State private var currentScale = 1.0 - @State private var previousScale = 0.0 - @State private var currentOffset: CGSize = .zero - @State private var previousOffset: CGSize = .zero - - var body: some View { - GeometryReader { geometry in - VStack { - image - .resizable() - .scaledToFit() - .scaleEffect(currentScale) - .offset(currentOffset) - .frame(width: screenWidth, height: screenHeight) - .clipped() - .gesture( - MagnifyGesture() - .onChanged { gesture in - withAnimation(.interactiveSpring()) { - currentScale = - previousScale + gesture.magnification - (previousScale == 0 ? 0 : 1) - } - } - .onEnded { _ in - previousScale = currentScale - } - .simultaneously( - with: DragGesture(minimumDistance: 0) - .onChanged { gesture in - withAnimation(.interactiveSpring()) { - var newOffset: CGSize = .zero - let offset = gesture.translation - - newOffset.width = offset.width + previousOffset.width - newOffset.height = offset.height + previousOffset.height - - currentOffset = clampOffset(offset: newOffset) - } - } - .onEnded { _ in - previousOffset = currentOffset - } - ) - ) - .highPriorityGesture( - TapGesture(count: 2) - .onEnded { - withAnimation { - currentScale = 1.0 - previousScale = 0 - currentOffset = .zero - previousOffset = .zero - } - } - ) - } - .onAppear { - screenWidth = geometry.size.width - screenHeight = geometry.size.height - } - } - } - - private func clampOffset(offset: CGSize = .zero) -> CGSize { - var newOffset = offset - - if currentScale > 1 { - let maxX = ((screenWidth * currentScale) - screenWidth) / 2 - let maxY = ((screenHeight * currentScale) - screenHeight) / 2 - - newOffset.width = min(max(-maxX, newOffset.width), maxX) - newOffset.height = min(max(-maxY, newOffset.height), maxY) - } else { - newOffset = .zero - } - - return newOffset - } -} - -#Preview { - ZoomableImageView(image: Image(systemName: "photo")) -} -- cgit v1.2.3