vibe coding screenshot sharing
This commit is contained in:
@ -1,162 +1,243 @@
|
||||
//
|
||||
// ShareViewController.swift
|
||||
// Haystack
|
||||
//
|
||||
// ShareViewController.swift
|
||||
// Haystack
|
||||
//
|
||||
// Created by Rio Keefe on 03/05/2025.
|
||||
// Created by Rio Keefe on 03/05/2025.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Social
|
||||
import MobileCoreServices
|
||||
import MobileCoreServices // For kUTTypeImage
|
||||
|
||||
class ShareViewController: SLComposeServiceViewController {
|
||||
|
||||
let appGroupName = "group.com.haystack.app" // Replace with your actual App Group identifier
|
||||
let tokenKey = "sharedAuthToken"
|
||||
let uploadURL = URL(string: "https://haystack.johncosta.tech/image/")!
|
||||
let uploadURLBase = URL(string: "https://haystack.johncosta.tech/image/")! // Base URL
|
||||
|
||||
var bearerToken: String?
|
||||
// Store the item provider to access it later in didSelectPost
|
||||
private var imageItemProvider: NSItemProvider?
|
||||
private var extractedImageName: String = "image" // Default name
|
||||
// Store a base name, extension will be determined during item loading
|
||||
private var baseImageName: String = "SharedImage" // A more descriptive default
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// Load the bearer token from the App Group in viewDidLoad
|
||||
// This is okay as reading from UserDefaults is fast
|
||||
if let sharedDefaults = UserDefaults(suiteName: appGroupName) {
|
||||
bearerToken = sharedDefaults.string(forKey: tokenKey)
|
||||
print("Retrieved bearer token: \(bearerToken ?? "nil")")
|
||||
} else {
|
||||
print("Error accessing App Group UserDefaults.")
|
||||
// Optionally inform the user or disable posting if token is crucial
|
||||
// self.isContentValid() could check if bearerToken is nil
|
||||
// Invalidate content if token is crucial and missing
|
||||
// This will be caught by isContentValid()
|
||||
}
|
||||
|
||||
// Store the item provider, but don't load the data synchronously yet
|
||||
if let item = extensionContext?.inputItems.first as? NSExtensionItem,
|
||||
let provider = item.attachments?.first as? NSItemProvider {
|
||||
if provider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
|
||||
self.imageItemProvider = provider
|
||||
// Attempt to get a suggested name early if available
|
||||
extractedImageName = provider.suggestedName ?? "image"
|
||||
if let dotRange = extractedImageName.range(of: ".", options: .backwards) {
|
||||
extractedImageName = String(extractedImageName[..<dotRange.lowerBound])
|
||||
}
|
||||
guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem,
|
||||
let provider = extensionItem.attachments?.first else {
|
||||
print("No attachments found.")
|
||||
// Invalidate content if no provider
|
||||
self.imageItemProvider = nil // Ensure it's nil so isContentValid fails
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
print("No image found.")
|
||||
// If no image is found, the content is not valid for this extension
|
||||
// You might want to adjust isContentValid() based on this
|
||||
if provider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
|
||||
self.imageItemProvider = provider
|
||||
// Attempt to get a suggested name early if available, and clean it.
|
||||
// This will be our default base name if the item itself doesn't provide a better one.
|
||||
if let suggested = provider.suggestedName, !suggested.isEmpty {
|
||||
if let dotRange = suggested.range(of: ".", options: .backwards) {
|
||||
self.baseImageName = String(suggested[..<dotRange.lowerBound])
|
||||
} else {
|
||||
self.baseImageName = suggested
|
||||
}
|
||||
}
|
||||
// Sanitize the base name slightly (remove problematic characters for a filename)
|
||||
let anInvalidCharacters = CharacterSet(charactersIn: "/\\?%*|\"<>:")
|
||||
self.baseImageName = self.baseImageName.components(separatedBy: anInvalidCharacters).joined(separator: "_")
|
||||
if self.baseImageName.isEmpty { self.baseImageName = "SharedImage" } // Ensure not empty
|
||||
|
||||
} else {
|
||||
print("Attachment is not an image.")
|
||||
self.imageItemProvider = nil // Ensure it's nil so isContentValid fails
|
||||
}
|
||||
}
|
||||
|
||||
override func isContentValid() -> Bool {
|
||||
// Content is valid only if we have an item provider for an image AND a bearer token
|
||||
return imageItemProvider != nil && bearerToken != nil
|
||||
let isValid = imageItemProvider != nil && bearerToken != nil
|
||||
if imageItemProvider == nil {
|
||||
print("isContentValid: imageItemProvider is nil")
|
||||
}
|
||||
if bearerToken == nil {
|
||||
print("isContentValid: bearerToken is nil")
|
||||
}
|
||||
return isValid
|
||||
}
|
||||
|
||||
override func didSelectPost() {
|
||||
// This method is called when the user taps the "Post" button.
|
||||
// Start the asynchronous operation here.
|
||||
|
||||
guard let provider = imageItemProvider else {
|
||||
print("Error: No image item provider found when posting.")
|
||||
// Inform the user or log an error
|
||||
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
informUserAndCancel(message: "No image found to share.")
|
||||
return
|
||||
}
|
||||
|
||||
guard let token = bearerToken else {
|
||||
print("Error: Bearer token is missing when posting.")
|
||||
// Inform the user or log an error
|
||||
extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
return
|
||||
print("Error: Bearer token is missing when posting.")
|
||||
informUserAndCancel(message: "Authentication error. Please try again.")
|
||||
return
|
||||
}
|
||||
|
||||
// Load the image data asynchronously
|
||||
// Start activity indicator or similar UI feedback
|
||||
// For SLComposeServiceViewController, the system provides some UI
|
||||
|
||||
provider.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil) { [weak self] (item, error) in
|
||||
guard let self = self else { return }
|
||||
|
||||
if let error = error {
|
||||
print("Error loading image data for upload: \(error.localizedDescription)")
|
||||
// Inform the user about the failure
|
||||
self.extensionContext!.cancelRequest(withError: error)
|
||||
self.informUserAndCancel(message: "Could not load image: \(error.localizedDescription)")
|
||||
return
|
||||
}
|
||||
|
||||
var rawImageData: Data?
|
||||
var finalImageName = self.extractedImageName // Use the name extracted earlier
|
||||
var imageData: Data?
|
||||
var finalImageNameWithExtension: String
|
||||
var mimeType: String = "application/octet-stream" // Default MIME type
|
||||
|
||||
if let url = item as? URL, let data = try? Data(contentsOf: url) {
|
||||
rawImageData = data
|
||||
// Refine the name extraction here if necessary, though doing it in viewDidLoad is also an option
|
||||
finalImageName = url.lastPathComponent
|
||||
if let dotRange = finalImageName.range(of: ".", options: .backwards) {
|
||||
finalImageName = String(finalImageName[..<dotRange.lowerBound])
|
||||
}
|
||||
// Determine base name (without extension)
|
||||
var currentBaseName = self.baseImageName // Use the one prepared in viewDidLoad
|
||||
if let suggested = provider.suggestedName, !suggested.isEmpty {
|
||||
if let dotRange = suggested.range(of: ".", options: .backwards) {
|
||||
currentBaseName = String(suggested[..<dotRange.lowerBound])
|
||||
} else {
|
||||
currentBaseName = suggested
|
||||
}
|
||||
let anInvalidCharacters = CharacterSet(charactersIn: "/\\?%*|\"<>:")
|
||||
currentBaseName = currentBaseName.components(separatedBy: anInvalidCharacters).joined(separator: "_")
|
||||
if currentBaseName.isEmpty { currentBaseName = "DefaultImageName" }
|
||||
}
|
||||
|
||||
|
||||
if let url = item as? URL {
|
||||
print("Image provided as URL: \(url)")
|
||||
finalImageNameWithExtension = url.lastPathComponent // Includes extension
|
||||
// Ensure baseName is updated if URL provides a different one
|
||||
if let dotRange = finalImageNameWithExtension.range(of:".", options: .backwards) {
|
||||
currentBaseName = String(finalImageNameWithExtension[..<dotRange.lowerBound])
|
||||
} else {
|
||||
currentBaseName = finalImageNameWithExtension // No extension in name
|
||||
}
|
||||
|
||||
do {
|
||||
imageData = try Data(contentsOf: url)
|
||||
// Determine MIME type from URL extension
|
||||
let pathExtension = url.pathExtension.lowercased()
|
||||
mimeType = self.mimeType(forPathExtension: pathExtension)
|
||||
if !finalImageNameWithExtension.contains(".") && !pathExtension.isEmpty { // if original lastPathComponent had no ext
|
||||
finalImageNameWithExtension = "\(currentBaseName).\(pathExtension)"
|
||||
} else if !finalImageNameWithExtension.contains(".") && pathExtension.isEmpty { // no ext anywhere
|
||||
finalImageNameWithExtension = "\(currentBaseName).jpg" // default to jpg
|
||||
mimeType = "image/jpeg"
|
||||
}
|
||||
|
||||
} catch {
|
||||
print("Error creating Data from URL: \(error)")
|
||||
self.informUserAndCancel(message: "Could not read image file.")
|
||||
return
|
||||
}
|
||||
} else if let image = item as? UIImage {
|
||||
print("Image provided as UIImage")
|
||||
// Prefer PNG for screenshots/UIImage, fallback to JPEG
|
||||
if let data = image.pngData() {
|
||||
imageData = data
|
||||
mimeType = "image/png"
|
||||
finalImageNameWithExtension = "\(currentBaseName).png"
|
||||
} else if let data = image.jpegData(compressionQuality: 0.9) { // Good quality JPEG
|
||||
imageData = data
|
||||
mimeType = "image/jpeg"
|
||||
finalImageNameWithExtension = "\(currentBaseName).jpg"
|
||||
} else {
|
||||
print("Could not convert UIImage to Data.")
|
||||
self.informUserAndCancel(message: "Could not process image.")
|
||||
return
|
||||
}
|
||||
} else if let data = item as? Data {
|
||||
rawImageData = data
|
||||
// Use the suggested name if available, fallback to default
|
||||
finalImageName = provider.suggestedName ?? "image"
|
||||
print("Image provided as Data")
|
||||
imageData = data
|
||||
// We have raw data, try to use suggestedName's extension or default to png/jpg
|
||||
var determinedExtension = "png" // Default
|
||||
if let suggested = provider.suggestedName,
|
||||
let dotRange = suggested.range(of: ".", options: .backwards) {
|
||||
let ext = String(suggested[dotRange.upperBound...]).lowercased()
|
||||
if ["png", "jpg", "jpeg", "gif"].contains(ext) {
|
||||
determinedExtension = ext
|
||||
}
|
||||
}
|
||||
mimeType = self.mimeType(forPathExtension: determinedExtension)
|
||||
finalImageNameWithExtension = "\(currentBaseName).\(determinedExtension)"
|
||||
|
||||
} else {
|
||||
print("Error: Could not get image data in a usable format.")
|
||||
// Inform the user about the failure
|
||||
self.extensionContext!.cancelRequest(withError: NSError(domain: "ShareExtension", code: -4, userInfo: [NSLocalizedDescriptionKey: "Could not process image data."]))
|
||||
return
|
||||
print("Error: Could not get image data in a usable format. Item type: \(type(of: item)) Item: \(String(describing: item))")
|
||||
self.informUserAndCancel(message: "Unsupported image format.")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let dataToUpload = rawImageData else {
|
||||
print("Error: No image data to upload.")
|
||||
// Inform the user about the failure
|
||||
self.extensionContext!.cancelRequest(withError: NSError(domain: "ShareExtension", code: -5, userInfo: [NSLocalizedDescriptionKey: "Image data is missing."]))
|
||||
return
|
||||
guard let dataToUpload = imageData else {
|
||||
print("Error: No image data to upload after processing.")
|
||||
self.informUserAndCancel(message: "Image data is missing.")
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure finalImageNameWithExtension is not just an extension like ".png"
|
||||
if finalImageNameWithExtension.starts(with: ".") {
|
||||
finalImageNameWithExtension = "\(self.baseImageName)\(finalImageNameWithExtension)"
|
||||
}
|
||||
if finalImageNameWithExtension.isEmpty || !finalImageNameWithExtension.contains(".") {
|
||||
// Fallback if somehow the name is bad
|
||||
finalImageNameWithExtension = "\(self.baseImageName).png"
|
||||
mimeType = "image/png"
|
||||
}
|
||||
|
||||
// Now perform the upload asynchronously
|
||||
self.uploadRawData(dataToUpload, imageName: finalImageName, bearerToken: token)
|
||||
print("Uploading image: \(finalImageNameWithExtension), MIME: \(mimeType), Size: \(dataToUpload.count) bytes")
|
||||
self.uploadRawData(dataToUpload, imageNameWithExtension: finalImageNameWithExtension, mimeType: mimeType, bearerToken: token)
|
||||
}
|
||||
|
||||
// Do not complete the request here.
|
||||
// The request will be completed in the uploadRawData completion handler.
|
||||
}
|
||||
|
||||
func uploadRawData(_ rawData: Data, imageName: String, bearerToken: String) {
|
||||
// bearerToken is guaranteed to be non-nil here due to the guard in didSelectPost
|
||||
func uploadRawData(_ rawData: Data, imageNameWithExtension: String, mimeType: String, bearerToken: String) {
|
||||
// The imageNameWithExtension should already include the correct extension.
|
||||
// The server URL seems to expect the filename as a path component.
|
||||
let uploadURL = uploadURLBase.appendingPathComponent(imageNameWithExtension)
|
||||
print("Final Upload URL: \(uploadURL.absoluteString)")
|
||||
|
||||
let uploadURLwithName = uploadURL.appendingPathComponent(imageName)
|
||||
|
||||
var request = URLRequest(url: uploadURLwithName)
|
||||
var request = URLRequest(url: uploadURL)
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = rawData
|
||||
request.setValue("application/oclet-stream", forHTTPHeaderField: "Content-Type")
|
||||
request.setValue(mimeType, forHTTPHeaderField: "Content-Type") // Use determined MIME type
|
||||
request.setValue("Bearer \(bearerToken)", forHTTPHeaderField: "Authorization")
|
||||
request.setValue("\(rawData.count)", forHTTPHeaderField: "Content-Length")
|
||||
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { [weak self] (data, response, error) in
|
||||
// **IMPORTANT:** Complete the extension request on the main thread
|
||||
DispatchQueue.main.async {
|
||||
print("Upload finished. Error: \(error?.localizedDescription ?? "None"), Response: \(response?.description ?? "None")")
|
||||
guard let self = self else { return }
|
||||
print("Upload finished. Error: \(error?.localizedDescription ?? "None")")
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
print("HTTP Status: \(httpResponse.statusCode)")
|
||||
if let responseData = data, let responseString = String(data: responseData, encoding: .utf8) {
|
||||
print("Response Data: \(responseString)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let error = error {
|
||||
// Handle upload error (e.g., show an alert to the user)
|
||||
print("Upload failed: \(error.localizedDescription)")
|
||||
self?.extensionContext!.cancelRequest(withError: error)
|
||||
self.informUserAndCancel(message: "Upload failed: \(error.localizedDescription)")
|
||||
} else if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
|
||||
// Handle non-success HTTP status codes
|
||||
let errorDescription = "Server returned status code \(httpResponse.statusCode)"
|
||||
let errorDescription = "Upload failed. Server returned status code \(httpResponse.statusCode)."
|
||||
print(errorDescription)
|
||||
self?.extensionContext!.cancelRequest(withError: NSError(domain: "ShareExtension", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: errorDescription]))
|
||||
}
|
||||
else {
|
||||
// Upload was successful
|
||||
print("Upload successful")
|
||||
// Complete the request when the upload is done
|
||||
self?.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
self.informUserAndCancel(message: errorDescription)
|
||||
} else {
|
||||
print("Upload successful for \(imageNameWithExtension)")
|
||||
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -164,9 +245,36 @@ class ShareViewController: SLComposeServiceViewController {
|
||||
}
|
||||
|
||||
override func configurationItems() -> [Any]! {
|
||||
// You can add items here if you want to allow the user to enter additional info
|
||||
// e.g., a text field for a caption.
|
||||
// This example only handles image upload, so no config items are needed.
|
||||
// No configuration items needed for this simple image uploader.
|
||||
return []
|
||||
}
|
||||
|
||||
// Helper to inform user and cancel request
|
||||
private func informUserAndCancel(message: String) {
|
||||
let error = NSError(domain: "com.haystack.ShareExtension", code: 0, userInfo: [NSLocalizedDescriptionKey: message])
|
||||
print("Informing user: \(message)")
|
||||
// You could present an alert here if SLComposeServiceViewController allows easy alert presentation.
|
||||
// For now, just cancel the request. The system might show a generic error.
|
||||
self.extensionContext!.cancelRequest(withError: error)
|
||||
}
|
||||
|
||||
// Helper to get MIME type from path extension
|
||||
private func mimeType(forPathExtension pathExtension: String) -> String {
|
||||
let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue()
|
||||
if let uti = uti {
|
||||
let mimeType = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue()
|
||||
if let mimeType = mimeType {
|
||||
return mimeType as String
|
||||
}
|
||||
}
|
||||
// Fallback for common types if UTType fails or for robustness
|
||||
switch pathExtension.lowercased() {
|
||||
case "jpg", "jpeg": return "image/jpeg"
|
||||
case "png": return "image/png"
|
||||
case "gif": return "image/gif"
|
||||
case "bmp": return "image/bmp"
|
||||
case "tiff", "tif": return "image/tiff"
|
||||
default: return "application/octet-stream" // Generic fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user