import NetworkImage import SwiftUI import UserNotifications struct PostDetailsImageView: View { @EnvironmentObject var settings: SettingsManager @EnvironmentObject var manager: BooruManager var url: URL? @Binding var loadingState: BooruPostLoadingState var finalLoadingState: BooruPostLoadingState let placeholder: () -> Placeholder let post: BooruPost? #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) if settings.enableShareShortcut { Button { guard let shareURL = url else { return } keyWindow?.rootViewController?.present( UIActivityViewController( activityItems: [shareURL], applicationActivities: nil ), animated: true ) } label: { Label("Share", systemImage: "square.and.arrow.up") } .disabled(url == nil) } #endif #if os(iOS) Button { guard let imageURL = url else { return } Task(priority: .userInitiated) { guard let imageData = await ImageCacheManager.shared.loadImageData(for: imageURL), let uiImage = UIImage(data: imageData) else { return } await MainActor.run { UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil) } } } label: { Label("Save to Photos", systemImage: "square.and.arrow.down") } #endif #if os(macOS) Button { saveImageToPicturesFolder() } label: { Label("Save to Pictures", systemImage: "square.and.arrow.down") } .keyboardShortcut("s", modifiers: [.command]) #endif Button { #if os(iOS) Task(priority: .userInitiated) { guard let imageURL = url else { return } guard let imageData = await ImageCacheManager.shared.loadImageData(for: imageURL), let uiImage = UIImage(data: imageData) else { return } await MainActor.run { UIPasteboard.general.image = uiImage } } #else if let url { NSPasteboard.general.clearContents() NSPasteboard.general.writeObjects([NSImage(byReferencing: url)]) } #endif } label: { Label("Copy", systemImage: "doc.on.doc") } .keyboardShortcut("c", modifiers: [.command]) Button { guard let postURL = postURL(for: post?.id) else { return } openURL(postURL) } label: { Label("Open Post in Safari", systemImage: "safari") } .disabled(postURL(for: post?.id) == nil) if let source = post?.source, let sourceURL = URL(string: source.trimmingCharacters(in: .whitespacesAndNewlines)) { Button { openURL(sourceURL) } label: { Label("Open Source Link 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 selectedPost = manager.selectedPost, let index = manager.postIndexMap[selectedPost.id], (0.., finalLoadingState: BooruPostLoadingState = .loadingFile, post: BooruPost? = nil, @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 self.post = post } private func postURL(for id: String?) -> URL? { guard let id, !id.isEmpty else { return nil } var components = URLComponents() components.scheme = "https" components.host = manager.domain switch manager.flavor { case .moebooru: components.path = "/post/show/\(id)" case .gelbooru: components.path = "/index.php" components.queryItems = [ URLQueryItem(name: "page", value: "post"), URLQueryItem(name: "s", value: "view"), URLQueryItem(name: "id", value: id), ] case .danbooru: components.path = "/posts/\(id)" } return components.url } #if os(macOS) @preconcurrency private func saveImageToPicturesFolder() { guard let url = self.url else { return } let provider = manager.provider let detailViewQuality = settings.detailViewQuality let saveTagsToFile = settings.saveTagsToFile let post = self.post URLSession.shared.dataTask(with: url) { data, _, _ in guard let data, let 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)_\(detailViewQuality.rawValue.lowercased()).\(url.pathExtension)" ) ) if 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 \(saveTagsToFile ? "and tags" : "") saved (\(post.id))" ) } #endif } catch { print("PostDetailsImageView.saveImageToPicturesFolder: \(error)") } } .resume() } #endif private func openURL(_ url: URL) { #if os(macOS) NSWorkspace.shared.open(url) #else UIApplication.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) } } }