import NetworkImage import SwiftUI import UserNotifications struct PostDetailsImageView: View { @EnvironmentObject var settings: SettingsManager var url: URL? @Binding var loadingState: BooruPostLoadingState var finalLoadingState: BooruPostLoadingState 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 @Binding private var selectedPost: (post: BooruPost?, manager: BooruManager?) #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 { let content = NetworkImage(url: url) { image in InteractiveImageView( image: image, contextMenu: Group { #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(macOS) Button { saveImageToPicturesFolder() } label: { Label("Save Image", systemImage: "square.and.arrow.down") } .keyboardShortcut("s", modifiers: [.command]) #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 Button { openURL(postURL(for: selectedPost.post?.id ?? "")) } label: { Label("Open Post in Safari", systemImage: "safari") } if let source = selectedPost.post?.source { Button { openURL(URL(string: source)!) } label: { Label("Open Source in Safari", systemImage: "safari") } } } ) .onAppear { if loadingState != .loaded { loadingState = finalLoadingState } } } placeholder: { placeholder() .onAppear { loadingState = .loadingPreview } } #if os(macOS) return content.overlay( Group { Button(action: saveImageToPicturesFolder) { EmptyView() } .keyboardShortcut("s", modifiers: [.command]) Button(action: { movePostCursor(by: 1) }) { EmptyView() } .keyboardShortcut(.rightArrow, modifiers: []) Button(action: { movePostCursor(by: -1) }) { EmptyView() } .keyboardShortcut(.leftArrow, modifiers: []) } .frame(width: 0, height: 0) .opacity(0) ) #else return content #endif } func movePostCursor(by direction: Int) { guard let post = selectedPost.post, let manager = selectedPost.manager, let index = manager.postIndexMap[post.id], (0.., selectedPost: Binding<(post: BooruPost?, manager: BooruManager?)>, finalLoadingState: BooruPostLoadingState = .loadingFile, @ViewBuilder placeholder: @escaping () -> Placeholder = { GeometryReader { _ 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.placeholder = placeholder _selectedPost = selectedPost } private func postURL(for id: String) -> URL { if let manager = selectedPost.manager { switch manager.flavor { case .moebooru: return URL(string: "https://\(manager.domain)/post/show/\(id)")! case .gelbooru: return URL(string: "https://\(manager.domain)/index.php?page=post&s=view&id=\(id)")! case .danbooru: return URL(string: "https://\(manager.domain)/posts/\(id)")! } } return URL(string: "#")! } #if os(macOS) private func saveImageToPicturesFolder() { guard let url = self.url, let manager = selectedPost.manager else { return } let provider = manager.provider URLSession.shared.dataTask(with: url) { data, _, _ in guard let data, let post = selectedPost.post else { return } let picturesURL = FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Pictures/Sora/\(provider.rawValue)") do { try FileManager.default.createDirectory( at: picturesURL, withIntermediateDirectories: true ) try data.write( to: picturesURL .appendingPathComponent( "\(post.id)_\(settings.detailViewQuality.rawValue.lowercased()).\(url.pathExtension)" ) ) if settings.saveTagsToFile { try post.tags.joined(separator: "\n").write( to: picturesURL.appendingPathComponent( "\(post.id).txt" ), atomically: true, encoding: .utf8 ) } #if os(macOS) Task { await sendLocalNotification( title: "Sora", body: "Image \(settings.saveTagsToFile ? "and tags" : "") saved (\(post.id))" ) } #endif } catch { print("PostDetailsImageView.saveImageToPicturesFolder: \(error)") } } .resume() } #endif private func openURL(_ url: URL) { #if os(iOS) UIApplication.shared.open(url) #else NSWorkspace.shared.open(url) #endif } private func sendLocalNotification(title: String, body: String) async { let notificationCenter = UNUserNotificationCenter.current() do { try await notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) } catch { debugPrint(error) } let content = UNMutableNotificationContent() content.title = title content.body = body content.sound = .default do { try await notificationCenter.add( UNNotificationRequest( identifier: UUID().uuidString, content: content, trigger: nil ) ) } catch { debugPrint(error) } } }