import SwiftUI import UniformTypeIdentifiers // Nodig voor UTType.image import Vision // Voor OCR functionaliteit // MARK: - Image Store (ObservableObject) class GalleryImageStore: ObservableObject { @Published var images: [IdentifiableImage] = [] // NIEUW: Permanent stash directory private static let stashDirectoryName = "StashItems" // NIEUW: Initialization flag private var hasInitialized = false // NIEUW: Set om bij te houden welke afbeeldingen momenteel worden toegevoegd private var processingIdentifiers = Set() init() { // Cleanup oude stash bestanden bij opstarten if !hasInitialized { cleanupStashDirectory() hasInitialized = true } } private var stashDirectory: URL { let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! let appDirectory = appSupportURL.appendingPathComponent("ShotScreen") let stashDir = appDirectory.appendingPathComponent(Self.stashDirectoryName) // Zorg ervoor dat de directory bestaat try? FileManager.default.createDirectory(at: stashDir, withIntermediateDirectories: true, attributes: nil) return stashDir } // NIEUW: Public accessor voor stash directory var publicStashDirectory: URL { return self.stashDirectory } // NIEUW: Helper functie om een unieke copy naam te genereren func generateUniqueCopyName(baseName: String) -> String { let existingNames = Set(images.compactMap { item in item.fileURL?.deletingPathExtension().lastPathComponent ?? item.customName }) // Probeer eerst zonder nummer let firstTry = "\(baseName) Copy" if !existingNames.contains(firstTry) { return firstTry } // Als die bestaat, probeer met nummers var counter = 2 while counter <= 999 { // Voorkom oneindige loop let attempt = "\(baseName) Copy \(counter)" if !existingNames.contains(attempt) { return attempt } counter += 1 } // Fallback als er teveel copies zijn return "\(baseName) Copy \(Int(Date().timeIntervalSince1970) % 10000)" } func addImage(_ nsImage: NSImage, fileURL: URL? = nil, suggestedName: String? = nil, skipDuplicateCheck: Bool = false) { let callId = UUID().uuidString.prefix(4) print("πŸ–ΌοΈ [\(callId)] addImage START. fileURL: \(fileURL?.lastPathComponent ?? "nil"), suggestedName: '\(suggestedName ?? "nil")', nsImage size: \(nsImage.size), skipDuplicateCheck: \(skipDuplicateCheck)") // Step 1: Generate unique identifier let imageIdentifier = generateImageIdentifier(from: nsImage, fileURL: fileURL, suggestedName: suggestedName, callId: String(callId)) // Step 2: Check if already processing guard !processingIdentifiers.contains(imageIdentifier) else { print("πŸ–ΌοΈ [\(callId)] GalleryImageStore: Afbeelding met identifier '\(imageIdentifier.prefix(50))...' wordt AL VERWERKT. Toevoegen overgeslagen.") return } // Step 3: Mark as processing processingIdentifiers.insert(imageIdentifier) print("⏳ [\(callId)] GalleryImageStore: Identifier '\(imageIdentifier.prefix(50))...' toegevoegd aan processing set.") // Step 4: Process on main queue DispatchQueue.main.async { self.processImageAddition(nsImage: nsImage, fileURL: fileURL, suggestedName: suggestedName, skipDuplicateCheck: skipDuplicateCheck, imageIdentifier: imageIdentifier, callId: String(callId)) } } // MARK: - Private Helper Methods for addImage private func generateImageIdentifier(from nsImage: NSImage, fileURL: URL?, suggestedName: String?, callId: String) -> String { if let url = fileURL, url.path.contains(self.stashDirectory.path) { let identifier = url.path print("πŸ–ΌοΈ [\(callId)] Generated imageIdentifier (from stash path): \(identifier)") return identifier } else if let tiffData = nsImage.tiffRepresentation { let identifier = "tiff_\(tiffData.count)_\(suggestedName ?? "no_name")_\(fileURL?.lastPathComponent ?? "no_url")" print("πŸ–ΌοΈ [\(callId)] Generated imageIdentifier (from TIFF data): \(identifier)") return identifier } else { print("⚠️ [\(callId)] GalleryImageStore: Kan geen unieke identifier voor afbeelding genereren. Toevoegen overgeslagen.") return "failed_\(callId)" } } private func processImageAddition(nsImage: NSImage, fileURL: URL?, suggestedName: String?, skipDuplicateCheck: Bool, imageIdentifier: String, callId: String) { print("πŸ–ΌοΈ [\(callId)] DispatchQueue.main.async - START check images.contains") // Step 1: Check for duplicates (unless skipped) let alreadyInStore = checkForDuplicates(nsImage: nsImage, fileURL: fileURL, skipDuplicateCheck: skipDuplicateCheck, callId: callId) if alreadyInStore { print("πŸ–ΌοΈ [\(callId)] Afbeelding BESTAAT AL in de store. Verwijder van processing set.") self.processingIdentifiers.remove(imageIdentifier) print("βŒ› [\(callId)] Identifier '\(imageIdentifier.prefix(50))...' verwijderd van processing set (bestond al).") return } // Step 2: Create permanent URL if needed guard let permanentURL = ensurePermanentURL(nsImage: nsImage, fileURL: fileURL, suggestedName: suggestedName, callId: callId) else { print("❌ [\(callId)] GalleryImageStore: Kon geen permanente URL verkrijgen/maken. Verwijder van processing set.") self.processingIdentifiers.remove(imageIdentifier) print("βŒ› [\(callId)] Identifier '\(imageIdentifier.prefix(50))...' verwijderd van processing set (URL error).") return } // Step 3: Create and add the new item let newItem = IdentifiableImage(nsImage: nsImage, fileURL: permanentURL, customName: suggestedName) print("πŸ–ΌοΈ [\(callId)] Nieuw IdentifiableImage aangemaakt: ID \(newItem.id.uuidString.prefix(4)), URL: \(newItem.fileURL?.lastPathComponent ?? "nil"), Name: \(newItem.customName ?? "nil")") self.images.append(newItem) print("πŸ–ΌοΈ [\(callId)] Afbeelding toegevoegd. URL: \(permanentURL.lastPathComponent). Nieuwe count: \(self.images.count). Verwijder van processing set.") self.processingIdentifiers.remove(imageIdentifier) print("βŒ› [\(callId)] Identifier '\(imageIdentifier.prefix(50))...' verwijderd van processing set (succesvol toegevoegd).") } private func checkForDuplicates(nsImage: NSImage, fileURL: URL?, skipDuplicateCheck: Bool, callId: String) -> Bool { if skipDuplicateCheck { print("πŸ”₯ [\(callId)] ULTRA MODE: Skipping duplicate check for intentional duplicate") return false } return self.images.contains { existingItem in print("πŸ–ΌοΈ [\(callId)] Checking existingItem: ID \(existingItem.id.uuidString.prefix(4)), URL: \(existingItem.fileURL?.lastPathComponent ?? "nil"), Name: \(existingItem.customName ?? "nil")") // Check URL match if let existingURL = existingItem.fileURL, let newURL = fileURL, existingURL.standardizedFileURL == newURL.standardizedFileURL { print("πŸ–ΌοΈ [\(callId)] Match on standardizedFileURL: \(newURL.path)") return true } // Check TIFF data match if let d1 = existingItem.nsImage.tiffRepresentation, let d2 = nsImage.tiffRepresentation, d1 == d2 { print("πŸ–ΌοΈ [\(callId)] Match on TIFF data.") return true } return false } } private func ensurePermanentURL(nsImage: NSImage, fileURL: URL?, suggestedName: String?, callId: String) -> URL? { var permanentURLToUse: URL? = fileURL if permanentURLToUse == nil || !FileManager.default.fileExists(atPath: permanentURLToUse!.path) || !permanentURLToUse!.path.contains(self.stashDirectory.path) { var finalSuggestedName = suggestedName if finalSuggestedName == nil || finalSuggestedName?.isEmpty == true { if let providedURL = fileURL { finalSuggestedName = providedURL.deletingPathExtension().lastPathComponent } else { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd_HH.mm.ss.SSS" finalSuggestedName = "Image_\(dateFormatter.string(from: Date()))" } print("πŸ–ΌοΈ [\(callId)] Geen/ongeldige naam of URL, gebruik (herleide) naam: '\(finalSuggestedName ?? "default_name")'") } let nameForURLCreation = finalSuggestedName ?? "default_for_url_\(callId)" print("πŸ–ΌοΈ [\(callId)] Maak permanente URL met effective name: '\(nameForURLCreation)'") permanentURLToUse = self.createPermanentStashURL(for: nsImage, suggestedName: nameForURLCreation, useOriginalName: true) } return permanentURLToUse } // NIEUW: Maak een permanente URL voor stash items private func createPermanentStashURL(for nsImage: NSImage, suggestedName: String?, useOriginalName: Bool) -> URL? { guard let tiffRepresentation = nsImage.tiffRepresentation, let bitmapImageRep = NSBitmapImageRep(data: tiffRepresentation), let pngData = bitmapImageRep.representation(using: .png, properties: [:]) else { print("❌ Could not create permanent stash URL: image conversion failed") return nil } // FIXED: Gebruik originele naam als useOriginalName true is en suggestedName bestaat let filename: String if useOriginalName, let originalName = suggestedName, !originalName.isEmpty { // Gebruik originele naam met .png extensie let baseFilename = "\(originalName).png" // Check voor duplicate namen var finalFilename = baseFilename var counter = 1 while FileManager.default.fileExists(atPath: stashDirectory.appendingPathComponent(finalFilename).path) { finalFilename = "\(originalName) (\(counter)).png" counter += 1 if counter > 100 { finalFilename = baseFilename break } } filename = finalFilename } else { // Default naar timestamp-based filename let timestamp = Int(Date().timeIntervalSince1970) let uuid = UUID().uuidString.prefix(8) filename = "StashItem_\(timestamp)_\(uuid).png" } let permanentURL = stashDirectory.appendingPathComponent(filename) do { try pngData.write(to: permanentURL) print("βœ… Created permanent stash URL: \(permanentURL.lastPathComponent)") return permanentURL } catch { print("❌ Could not create permanent stash URL: \(error)") return nil } } // Helper om de store leeg te maken en een initiΓ«le afbeelding in te stellen (optioneel) func setInitialImage(_ nsImage: NSImage?) { DispatchQueue.main.async { self.images.removeAll() if let img = nsImage { // AANGEPAST: Ook initiΓ«le image krijgt permanente URL let permanentURL = self.createPermanentStashURL(for: img, suggestedName: nil, useOriginalName: true) self.images.append(IdentifiableImage(nsImage: img, fileURL: permanentURL)) } print("πŸ–ΌοΈ GalleryImageStore: Initial image set. Count: \(self.images.count)") } } func removeImage(_ imageItem: IdentifiableImage) { // NIEUW: Verwijder ook het permanente bestand if let fileURL = imageItem.fileURL { try? FileManager.default.removeItem(at: fileURL) print("πŸ—‘οΈ Removed permanent stash file: \(fileURL.lastPathComponent)") } self.images.removeAll { $0.id == imageItem.id } print("πŸ–ΌοΈ GalleryImageStore: Image removed. New count: \(self.images.count)") } func renameImage(item: IdentifiableImage, newName: String) -> Bool { guard let index = images.firstIndex(where: { $0.id == item.id }) else { return false } // NIEUW: Update ook de filename van het permanente bestand if let oldURL = item.fileURL { let newFilename = "\(newName).png" let newURL = oldURL.deletingLastPathComponent().appendingPathComponent(newFilename) do { try FileManager.default.moveItem(at: oldURL, to: newURL) // Update het item met de nieuwe URL en naam images[index].fileURL = newURL images[index].customName = newName print("βœ… Renamed stash file from \(oldURL.lastPathComponent) to \(newURL.lastPathComponent)") return true } catch { print("❌ Failed to rename stash file: \(error)") // Als bestand hernoemen faalt, update alleen de customName images[index].customName = newName print("ℹ️ Updated display name only (file rename failed)") return true } } else { // Geen fileURL, update alleen de customName images[index].customName = newName print("ℹ️ Updated display name only (no file URL)") return true } } // Methode om een item te verversen (bijv. na inline bewerking zoals BG remove) func refreshItem(_ item: IdentifiableImage) { // Dit is een placeholder. Afhankelijk van hoe IdentifiableImage en SwiftUI updates werken, // kan het zijn dat je het object moet vervangen of simpelweg de array moet publiceren. // Als IdentifiableImage een class is en @Published wordt gebruikt, kan een simpele property change voldoende zijn. // Als het een struct is, moet je het item in de 'images' array vervangen. if let index = images.firstIndex(where: { $0.id == item.id }) { // Forceer een update door het object (of een kopie met dezelfde id) opnieuw toe te wijzen // Dit is een beetje een hack, een beter ObservableObject-patroon is aanbevolen. let updatedItem = images[index] // Neem aan dat het een class is en de interne state is gewijzigd images.remove(at: index) images.insert(updatedItem, at: index) print("πŸ”„ GalleryImageStore: Refreshed item \(item.id)") } } // NIEUW: Cleanup functie om oude stash bestanden te verwijderen func cleanupStashDirectory() { do { let contents = try FileManager.default.contentsOfDirectory(at: stashDirectory, includingPropertiesForKeys: [.creationDateKey]) // Verwijder bestanden die niet meer in de images array staan let activeURLs = Set(images.compactMap { $0.fileURL }) for fileURL in contents { if !activeURLs.contains(fileURL) { try? FileManager.default.removeItem(at: fileURL) print("🧹 Cleaned up orphaned stash file: \(fileURL.lastPathComponent)") } } } catch { print("⚠️ Could not cleanup stash directory: \(error)") } } } // Hulpstructuur om NSImage identificeerbaar te maken voor de ForEach struct IdentifiableImage: Identifiable { let id = UUID() let nsImage: NSImage var fileURL: URL? = nil // AANGEPAST: nu altijd een permanente URL voor stash items var customName: String? = nil // huidige naam (kan via rename wijzigen) } // MARK: - Window Dragging Support // Deze NSViewRepresentable host onze CustomDragNSView struct WindowDragView: NSViewRepresentable { func makeNSView(context: Context) -> CustomDragNSView { return CustomDragNSView() } func updateNSView(_ nsView: CustomDragNSView, context: Context) { // Geen updates nodig voor nu } } // Deze NSView vangt muis events af om het venster te kunnen slepen class CustomDragNSView: NSView { private var mouseDownScreenLocation: NSPoint? private var initialWindowOrigin: NSPoint? override func mouseDown(with event: NSEvent) { guard let window = self.window else { return } self.mouseDownScreenLocation = NSEvent.mouseLocation self.initialWindowOrigin = window.frame.origin } override func mouseDragged(with event: NSEvent) { guard let window = self.window, let mouseDownScreenLoc = self.mouseDownScreenLocation, let initialWinOrigin = self.initialWindowOrigin else { return } let currentMouseScreenLocation = NSEvent.mouseLocation let deltaX = currentMouseScreenLocation.x - mouseDownScreenLoc.x let deltaY = currentMouseScreenLocation.y - mouseDownScreenLoc.y let newWindowOriginX = initialWinOrigin.x + deltaX let newWindowOriginY = initialWinOrigin.y + deltaY window.setFrameOrigin(NSPoint(x: newWindowOriginX, y: newWindowOriginY)) } override func mouseUp(with event: NSEvent) { self.mouseDownScreenLocation = nil self.initialWindowOrigin = nil } } // Representable voor NSVisualEffectView zodat we hem in SwiftUI kunnen gebruiken struct VisualEffectBackground: NSViewRepresentable { var material: NSVisualEffectView.Material = .hudWindow var blending: NSVisualEffectView.BlendingMode = .behindWindow var alpha: CGFloat = 1.0 func makeNSView(context: Context) -> NSVisualEffectView { let view = NSVisualEffectView() view.material = material view.blendingMode = blending view.state = .active view.alphaValue = alpha return view } func updateNSView(_ nsView: NSVisualEffectView, context: Context) { nsView.material = material nsView.blendingMode = blending nsView.alphaValue = alpha } } struct IntegratedGalleryView: View { @ObservedObject var imageStore: GalleryImageStore let initialImage: NSImage? let initialImageURL: URL? let initialImageName: String? let hostingWindow: NSWindow? let closeAction: () -> Void // MARK: - State Variables @State private var showPreview = false @State private var hoveredImageID: UUID? = nil @State private var targetedImageID: UUID? = nil // <-- TOEGEVOEGD VOOR PRECISIE @State private var isTargeted = false @State private var windowWidth: CGFloat = 200 // Default, wordt bijgewerkt @State private var windowHeight: CGFloat = 300 // Default, wordt bijgewerkt @State private var refreshTrigger = 0 @State private var isLayoutDebugEnabled = false private let debugUpdateInterval = 1.0 @State private var timer: Timer? @State private var refreshPreviewTimer: Timer? // πŸ”₯ NIEUW: Drop Zone fade state @State private var dropZoneOpacity: Double = 1.0 @State private var dropZoneFadeTimer: Timer? // MARK: - More State Variables @State private var currentPreviewName: String = "Unknown" // TURBO FIX: Instant naam tracking @State private var previewNameRefreshTimer: Timer? // ⏰ TIMER FORCE REFRESH @State private var imageForPreview: NSImage? @State private var isPreviewStable: Bool = false @State private var hoverWorkItem: DispatchWorkItem? // State voor het preview venster @State private var isClosingGracefully = false @State private var previewWindowController: NSWindowController? = nil // NIEUW: Stable state management to prevent excessive re-rendering @State private var renderIsolationID = UUID() @State private var didGridHandleDrop: Bool = false // ID voor de view @State private var _galleryID = UUID() var galleryID: UUID { _galleryID } // NIEUW: Cache voor StashDragDelegate instances @State private var dragDelegateCache: [UUID: StashDragDelegate] = [:] // CRITICAL FIX: Stable delegate cache that persists across renders @State private var stableDelegateCache: [UUID: StashDragDelegate] = [:] // CRITICAL FIX: Prevent cascade re-renders with render isolation @State private var lastImageCount: Int = 0 // Instellingen voor de grid en venstergrootte private let cellSpacing: CGFloat = 8 private let paddingAroundGrid: CGFloat = 5 private let titleBarHeight: CGFloat = 18 private let defaultEmptyWidth: CGFloat = 100 private let defaultEmptyHeight: CGFloat = 100 private var calculatedThumbnailSize: CGFloat { let baseSize: CGFloat = 70 return baseSize } private let thumbnailCornerRadius: CGFloat = 4 private let hoverScaleEffect: CGFloat = 1.03 // Haal border width op uit SettingsManager (wordt nu automatisch geΓΌpdatet door @ObservedObject) // private var stashBorderWidth: CGFloat { settings.stashWindowBorderWidth } // Deze kan weg of blijven // private var stashBorderColor: Color { Color.gray.opacity(0.5) } // Deze kan weg of blijven private var gridColumns: [GridItem] { Array(repeating: GridItem(.flexible(), spacing: cellSpacing), count: actualNumberOfColumns()) } // Initializer om de store te ontvangen // De initialImage parameter is voor het gemak om de store direct te vullen bij creatie. init(imageStore: GalleryImageStore, initialImage: NSImage? = nil, initialImageURL: URL? = nil, initialImageName: String? = nil, hostingWindow: NSWindow?, closeAction: @escaping () -> Void) { let callId = UUID().uuidString.prefix(4) print("🎨 [\(callId)] IntegratedGalleryView.INIT CALLED.") print("🎨 [\(callId)] imageStore.images.count: \(imageStore.images.count)") print("🎨 [\(callId)] initialImage isNil: \(initialImage == nil)") if let img = initialImage { print("🎨 [\(callId)] initialImage size: \(img.size)") } print("🎨 [\(callId)] initialImageURL: \(initialImageURL?.lastPathComponent ?? "nil")") print("🎨 [\(callId)] initialImageName: '\(initialImageName ?? "nil")'") print("🎨 [\(callId)] hostingWindow isNil: \(hostingWindow == nil)") self.imageStore = imageStore self.initialImage = initialImage self.initialImageURL = initialImageURL self.initialImageName = initialImageName self.hostingWindow = hostingWindow self.closeAction = closeAction if let imgToAdd = initialImage { let imageNameForLog = initialImageName ?? initialImageURL?.lastPathComponent ?? "Unnamed InitialImage (from init)" print("🎨 [\(callId)] Overweegt initialImage '\(imageNameForLog)' voor toevoeging.") var alreadyExistsReason = "None" let alreadyExists = imageStore.images.contains { existingItem in print("🎨 [\(callId)] INIT: Comparing with existingItem ID \(existingItem.id.uuidString.prefix(4)), URL: \(existingItem.fileURL?.lastPathComponent ?? "nil"), Name: \(existingItem.customName ?? "nil")") if let newURL = initialImageURL, newURL.isFileURL, let existingURL = existingItem.fileURL, existingURL.isFileURL, newURL.standardizedFileURL == existingURL.standardizedFileURL { if FileManager.default.fileExists(atPath: newURL.path) { print("🎨 [\(callId)] INIT: Match on standardizedFileURL: \(newURL.path)") alreadyExistsReason = "Matched standardizedFileURL: \(newURL.path)" return true } } if let newTIFF = imgToAdd.tiffRepresentation, let existingTIFF = existingItem.nsImage.tiffRepresentation, newTIFF == existingTIFF { print("🎨 [\(callId)] INIT: Match on TIFF data.") alreadyExistsReason = "Matched TIFF data" return true } return false } print("🎨 [\(callId)] INIT: alreadyExists check result: \(alreadyExists). Reason: \(alreadyExistsReason)") if !alreadyExists { print("βœ… [\(callId)] INIT: InitialImage '\(imageNameForLog)' wordt toegevoegd aan store.") // πŸ”₯ CRITICAL FIX: Skip duplicate check for initial stash images to allow BGR duplicates imageStore.addImage(imgToAdd, fileURL: initialImageURL, suggestedName: initialImageName, skipDuplicateCheck: true) } else { print("ℹ️ [\(callId)] INIT: InitialImage '\(imageNameForLog)' overgeslagen - bestaat al (Reason: \(alreadyExistsReason)).") } } else { print("ℹ️ [\(callId)] INIT: Geen initialImage meegegeven.") } print("🎨 [\(callId)] IntegratedGalleryView.INIT COMPLETED. imageStore.images.count: \(self.imageStore.images.count)") } var body: some View { // CRITICAL FIX: Render isolation - only log significant changes let currentImageCount = imageStore.images.count let shouldLogRender = currentImageCount != lastImageCount let _ = shouldLogRender ? print("🎨 IntegratedGalleryView.BODY: SIGNIFICANT CHANGE - Image count: \(currentImageCount) -> \(lastImageCount)") : () ZStack { // Root ZStack if isClosingGracefully { EmptyView() } else { VStack(spacing: 0) { // Custom Title Bar - ULTRA FIX: Altijd zichtbaar voor consistente layout HStack { Button(action: { self.closeAction() }) { Image(systemName: "xmark") .foregroundColor(Color.adaptivePrimaryText) .font(.system(size: 10, weight: .bold)) } .buttonStyle(.plain) .padding(.horizontal, 10) Spacer() // 🎯 NIEUW: Shake Test Button - NU RECHTS UITGELIJND Button(action: { testShakeAnimation() }) { Image(systemName: "bolt.fill") .foregroundColor(.yellow) .font(.system(size: 10, weight: .bold)) } .buttonStyle(.plain) .padding(.horizontal, 10) .help("Test Shake Animation") } .frame(height: titleBarHeight) .background( ZStack { VisualEffectBackground(alpha: 0.8) .cornerRadius(8) VisualEffectBackground(alpha: 0.4) .cornerRadius(8) WindowDragView() } ) .transition(.opacity) // Content Area - ULTRA FIX: Altijd dezelfde layout VStack(spacing: 0) { consistentImageGridView permanentFileDropZone } .frame(maxWidth: .infinity, maxHeight: .infinity) } } } .frame(width: effectiveWindowWidth(), height: effectiveWindowHeight()) .background(Color.clear) .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(Color.clear, lineWidth: 0) ) .animation(.easeInOut(duration: 0.2), value: imageStore.images.isEmpty) .onChange(of: showPreview) { newValue in handlePreviewWindowState(shouldShow: newValue) } .onChange(of: imageForPreview) { newImage in if showPreview, let _ = newImage { handlePreviewWindowState(shouldShow: true) } } .onChange(of: imageStore.images.count) { newCount in // CRITICAL FIX: Update tracking variables DispatchQueue.main.async { if self.lastImageCount != newCount { self.lastImageCount = newCount self.renderIsolationID = UUID() print("πŸ”„ CRITICAL: Image count changed to \(newCount), updated render isolation") // πŸ”₯ MEGA ULTRA FIX: If count increased and we're showing preview, update to show newest item if newCount > self.lastImageCount && self.showPreview && self.imageStore.images.count > 0 { if let newestItem = self.imageStore.images.last { let newestName = newestItem.fileURL?.deletingPathExtension().lastPathComponent ?? "Unknown" print("πŸ”₯ MEGA: New item detected during preview! Updating name to: '\(newestName)'") self.currentPreviewName = newestName // Force immediate preview update self.forceRefreshPreviewName() } } } } } .onReceive(NotificationCenter.default.publisher(for: NSApplication.willTerminateNotification)) { _ in closePreviewWindow() } .onReceive(NotificationCenter.default.publisher(for: .stashPreviewSizeChanged)) { _ in // πŸ”₯πŸ’Ž MEGA LIVE UPDATE VOOR PREVIEW SIZE! πŸ’ŽπŸ”₯ print("πŸ”₯ STASH: Preview size setting changed - will apply on next hover") // De nieuwe size wordt automatisch toegepast bij de volgende hover omdat we // SettingsManager.shared.stashPreviewSize.percentage gebruiken in de size calculations } .onReceive(NotificationCenter.default.publisher(for: .stashGridModeChanged)) { _ in // πŸ”₯πŸ’₯⚑ HYPERMODE GRID MODE LIVE UPDATE! ⚑πŸ’₯πŸ”₯ print("πŸ”₯ HYPERMODE: Grid mode changed - triggering layout update") DispatchQueue.main.async { self.renderIsolationID = UUID() // Force render update } } .onReceive(NotificationCenter.default.publisher(for: .stashGridConfigChanged)) { _ in // πŸ”₯πŸ’₯⚑ HYPERMODE GRID CONFIG LIVE UPDATE! ⚑πŸ’₯πŸ”₯ print("πŸ”₯ HYPERMODE: Grid configuration changed - triggering layout update") DispatchQueue.main.async { self.renderIsolationID = UUID() // Force render update // Force window size recalculation by updating hosting window if let window = self.hostingWindow { let newWidth = self.effectiveWindowWidth() let newHeight = self.effectiveWindowHeight() let newSize = NSSize(width: newWidth, height: newHeight) print("πŸ”₯ HYPERMODE: Updating window size to \(newWidth) x \(newHeight)") window.setContentSize(newSize) } } } .onAppear { self.lastImageCount = imageStore.images.count print("πŸ”Ά IntegratedGalleryView: onAppear - initialized with \(imageStore.images.count) images") } .onDisappear { closePreviewWindow() stopPreviewNameRefreshTimer() // ⏰ Stop timer on disappear dropZoneFadeTimer?.invalidate() // πŸ”₯ CLEANUP FADE TIMER imageStore.cleanupStashDirectory() } .padding(2) .onChange(of: hoveredImageID) { _ in // 🏎️ SUPER FAST HOVER FIX: Whenever hoveredImageID changes while preview is visible, force an immediate update. if showPreview { print("⚑ FAST HOVER: hoveredImageID changed, forcing preview update") handlePreviewWindowState(shouldShow: true) } } } // MARK: - Subviews // NIEUW: Permanente file drop zone die altijd zichtbaar is + FADE EFFECT private var permanentFileDropZone: some View { HStack { Spacer() Text("Drop Zone") .font(.caption) .fontWeight(.medium) .italic() // Cursief .foregroundColor(.secondary) // TERUG NAAR WIT voor contrast Spacer() } .background(isTargeted ? Color.blue.opacity(0.5) : Color.blue.opacity(0.4)) // Donkerblauw in plaats van grijs .clipShape( UnevenRoundedRectangle( topLeadingRadius: 0, bottomLeadingRadius: 6, bottomTrailingRadius: 6, topTrailingRadius: 0 ) ) .opacity(dropZoneOpacity) // πŸ”₯ FADE EFFECT .onDrop(of: [UTType.fileURL], isTargeted: .constant(false)) { providers -> Bool in handleFileOnlyDrop(providers: providers) return true } .onAppear { print("⏰ Drop Zone: permanentFileDropZone.onAppear TRIGGERED!") startDropZoneFadeTimer() } // VERWIJDERD: Alle padding weggehaald voor directe verbinding met thumbnails } private var dropZoneView: some View { VStack(spacing: 12) { // NIEWE: Speciale file drop zone die originele namen behoudt VStack(spacing: 6) { Image(systemName: "doc.on.doc") .font(.system(size: 20)) .foregroundColor(.blue.opacity(0.4)) // Veel donkerder blauw Text("Sleep bestanden hier") .font(.caption2) .fontWeight(.medium) Text("Behoudt originele naam") .font(.caption2) .foregroundColor(.secondary) } .frame(maxWidth: .infinity) .frame(height: 60) // Veel lager van 80 naar 60 .background(isTargeted ? Color.blue.opacity(0.4) : Color.blue.opacity(0.3)) // Veel donkerdere achtergrond .cornerRadius(8) .onDrop(of: [UTType.fileURL], isTargeted: .constant(false)) { providers -> Bool in handleFileOnlyDrop(providers: providers) return true } Text("of") .font(.caption2) .foregroundColor(.secondary) // Bestaande algemene drop zone VStack(spacing: 8) { Image(systemName: "photo.on.rectangle") .font(.system(size: 20)) .foregroundColor(.gray) Text("Sleep afbeeldingen hier") .font(.caption) .foregroundColor(.secondary) } .frame(maxWidth: .infinity) .frame(height: 60) .background(isTargeted ? Color.gray.opacity(0.15) : Color.gray.opacity(0.05)) .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .strokeBorder(isTargeted ? Color.gray.opacity(0.5) : Color.gray.opacity(0.3), style: StrokeStyle(lineWidth: 1, dash: [4])) ) .onDrop(of: [UTType.image], isTargeted: $isTargeted) { providers -> Bool in handleDrop(providers: providers) return true } } .padding(paddingAroundGrid) } private var consistentImageGridView: some View { ScrollView(.vertical, showsIndicators: false) { // CRITICAL FIX: Only log when count actually changes let currentCount = imageStore.images.count let _ = currentCount != lastImageCount ? print("🎨 GRID: Actual count change detected: \(lastImageCount) -> \(currentCount)") : () LazyVGrid(columns: gridColumns, spacing: cellSpacing) { if imageStore.images.isEmpty { // ULTRA FIX: Placeholder that looks like a thumbnail for consistent layout emptyStatePlaceholder } else { ForEach(imageStore.images) { imgItem in OptimizedStashItemView( imageItem: imgItem, calculatedThumbnailSize: calculatedThumbnailSize, thumbnailCornerRadius: thumbnailCornerRadius, hoverScaleEffect: hoverScaleEffect, hoveredImageID: $hoveredImageID, currentPreviewName: $currentPreviewName, imageForPreview: $imageForPreview, showPreview: $showPreview, isPreviewStable: $isPreviewStable, hoverWorkItem: $hoverWorkItem, stableDelegateCache: $stableDelegateCache, imageStore: imageStore, onRemove: { removedItem in stableDelegateCache.removeValue(forKey: removedItem.id) imageStore.removeImage(removedItem) } ) } } } .padding(.horizontal, paddingAroundGrid) .padding(.top, paddingAroundGrid) } .coordinateSpace(name: "GalleryScrollViewSpace") } // ULTRA FIX: Placeholder die er precies zo uitziet als een echte thumbnail private var emptyStatePlaceholder: some View { // ULTRA FIX: Helemaal leeg, 100% transparant Rectangle() .fill(Color.clear) .frame(width: calculatedThumbnailSize, height: calculatedThumbnailSize) .background(Color.clear) // 100% transparant zoals gevraagd .cornerRadius(thumbnailCornerRadius) .onDrop(of: [UTType.fileURL], isTargeted: $isTargeted) { providers -> Bool in // ULTRA FIX: Alleen handleFileOnlyDrop voor consistente naamgeving (net als onderste dropzone) handleFileOnlyDrop(providers: providers) return true } } // AANGEPAST: Veel eenvoudiger omdat stash items nu permanente URLs hebben private func createTempURLForStashItem(_ imgItem: IdentifiableImage) -> URL? { // REMOVED: This function is now handled by OptimizedStashItemView return imgItem.fileURL } // NIEUW: Open image in system app private func openImageInSystemApp(_ imgItem: IdentifiableImage, tempURL: URL) { // REMOVED: This function is now handled by OptimizedStashItemView NSWorkspace.shared.open(tempURL) } // NIEUW: Get display name for stash item private func displayNameForStashItem(_ imgItem: IdentifiableImage) -> String? { // REMOVED: This function is now handled by OptimizedStashItemView if let customName = imgItem.customName, !customName.isEmpty { return customName } if let fileURL = imgItem.fileURL { return fileURL.deletingPathExtension().lastPathComponent } return nil } // MARK: - Layout Calculation Functions (gebruiken nu imageStore.images.count etc.) private func titleBarHeightIfVisible() -> CGFloat { // ULTRA FIX: Titel bar is nu altijd zichtbaar voor consistente layout return titleBarHeight } private func actualNumberOfColumns() -> Int { // πŸ”₯πŸ’₯⚑ HYPERMODE FLEX GRID SYSTEM! ⚑πŸ’₯πŸ”₯ let settings = SettingsManager.shared let itemCount = max(1, imageStore.images.count) // Minimaal 1 voor consistente layout switch settings.stashGridMode { case .fixedColumns: // Fixed columns mode: max X columns, auto rows (original system) let maxCols = settings.stashMaxColumns return min(max(1, itemCount), maxCols) case .fixedRows: // Fixed rows mode: max Y rows, auto columns (horizontal growth!) let maxRows = settings.stashMaxRows return Int(ceil(Double(itemCount) / Double(maxRows))) } } private func totalNumberOfRowsForAllItems() -> Int { // πŸ”₯πŸ’₯⚑ HYPERMODE FLEX GRID ROWS CALCULATION! ⚑πŸ’₯πŸ”₯ let settings = SettingsManager.shared let itemCount = max(1, imageStore.images.count) // Minimaal 1 voor consistente layout switch settings.stashGridMode { case .fixedColumns: // Fixed columns mode: calculate rows based on columns let numCols = actualNumberOfColumns() guard numCols > 0 else { return 1 } return Int(ceil(Double(itemCount) / Double(numCols))) case .fixedRows: // Fixed rows mode: fixed Y rows, never more! let maxRows = settings.stashMaxRows return min(maxRows, itemCount) // Never exceed max rows setting } } private func effectiveWindowWidth() -> CGFloat { // ULTRA FIX: Gebruik altijd grid calculations, ook voor empty state let numCols = CGFloat(actualNumberOfColumns()) let thumbSize = self.calculatedThumbnailSize let totalImageWidth = numCols * thumbSize let totalSpacingWidth = max(0, numCols - 1) * cellSpacing return totalImageWidth + totalSpacingWidth + (2 * paddingAroundGrid) } private func effectiveWindowHeight() -> CGFloat { // ULTRA FIX: Gebruik altijd grid calculations, ook voor empty state let numRows = CGFloat(totalNumberOfRowsForAllItems()) let thumbSize = self.calculatedThumbnailSize let totalImageHeight = numRows * thumbSize let totalSpacingHeight = max(0, numRows - 1) * cellSpacing let dropZoneHeight: CGFloat = 18.0 // AANGEPAST: Slanke dropzone hoogte (tekst ~16px + kleine marge) // AANGEPAST: Gebruik echte padding waarden van imageGridView let topPadding = paddingAroundGrid // 5px let bottomGridPadding: CGFloat = 0 // 0px - imageGridView heeft geen bottom padding meer let dropZoneBottomPadding: CGFloat = 0 // 0px - Geen padding meer op dropzone let totalPadding = topPadding + bottomGridPadding + dropZoneBottomPadding // 5 + 0 + 0 = 5px (was 10px) return totalImageHeight + totalSpacingHeight + totalPadding + titleBarHeightIfVisible() + dropZoneHeight } // MARK: - Drop Handling // NIEUW: Speciale file-only drop handler die originele namen behoudt private func handleFileOnlyDrop(providers: [NSItemProvider]) { print("🎯 handleFileOnlyDrop called with \(providers.count) providers - FILENAME PRESERVATION MODE") for provider in providers { // Alleen file URLs accepteren if provider.hasItemConformingToTypeIdentifier(UTType.fileURL.identifier) { print("🎯 Found fileURL type in file-only drop") provider.loadItem(forTypeIdentifier: UTType.fileURL.identifier, options: nil) { (item, error) in if let error = error { print("❌ Error loading file URL: \(error)") return } print("🎯 Got item type: \(type(of: item)), value: \(String(describing: item))") var fileURL: URL? = nil var filename: String? = nil // VERBETERDE: Verschillende manieren om URL en naam te krijgen (same as handleDrop) if let url = item as? URL { print("πŸ”„ Direct URL: \(url.lastPathComponent)") fileURL = url filename = url.deletingPathExtension().lastPathComponent } else if let data = item as? Data { print("πŸ” Got Data (\(data.count) bytes), trying to decode...") // Probeer als URL data if let url = URL(dataRepresentation: data, relativeTo: nil) { print("πŸ”„ Decoded URL from data: \(url.lastPathComponent)") fileURL = url filename = url.deletingPathExtension().lastPathComponent } else if let string = String(data: data, encoding: .utf8) { print("πŸ” Data as string: '\(string)'") // Probeer string als file path let url = URL(string: string) ?? URL(fileURLWithPath: string) print("πŸ”„ Decoded URL from string: \(url.lastPathComponent)") fileURL = url filename = url.deletingPathExtension().lastPathComponent } } else if let string = item as? String { print("πŸ” Got string: '\(string)'") let url = URL(string: string) ?? URL(fileURLWithPath: string) print("πŸ”„ URL from string: \(url.lastPathComponent)") fileURL = url filename = url.deletingPathExtension().lastPathComponent } else if let array = item as? [String] { print("πŸ” Got string array: \(array)") if let firstPath = array.first { let url = URL(string: firstPath) ?? URL(fileURLWithPath: firstPath) print("πŸ”„ URL from array: \(url.lastPathComponent)") fileURL = url filename = url.deletingPathExtension().lastPathComponent } } else if let nsArray = item as? NSArray { print("πŸ” Got NSArray: \(nsArray)") if let firstPath = nsArray.firstObject as? String { let url = URL(string: firstPath) ?? URL(fileURLWithPath: firstPath) print("πŸ”„ URL from NSArray: \(url.lastPathComponent)") fileURL = url filename = url.deletingPathExtension().lastPathComponent } } else { print("❌ Unknown item type for file-only drop: \(type(of: item))") return } // Als we een file URL hebben, verwerk het bestand if let url = fileURL, let name = filename { print("βœ… Successfully extracted file URL and name: \(name)") // Check of het een image bestand is let imageExtensions = ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "webp", "heic"] let fileExtension = url.pathExtension.lowercased() guard imageExtensions.contains(fileExtension) else { print("⚠️ Not an image file: \(fileExtension)") return } DispatchQueue.main.async { self.copyFileToStash(fileURL: url, preservedName: name, shouldUpdateUI: true) } } else { print("❌ Could not extract valid file URL and name from item") } } } } } // UNIFIED: Single method for all stash file operations (replaces copyFileDirectlyToStashAndAdd) private func copyFileToStash(fileURL: URL, preservedName: String? = nil, shouldUpdateUI: Bool = false) { let finalName = preservedName ?? fileURL.deletingPathExtension().lastPathComponent print("πŸ”„ Copying file to stash: \(fileURL.lastPathComponent) as '\(finalName)' (updateUI: \(shouldUpdateUI))") // ULTRA FIX: Check if this is already a stash item being dragged within stash let stashDir = imageStore.publicStashDirectory if fileURL.path.contains(stashDir.path) { print("πŸ”₯ ULTRA MODE: Detected INTERNAL stash drag - creating duplicate instead of copy") // Find the existing stash item if let existingStashItem = imageStore.images.first(where: { $0.fileURL?.path == fileURL.path }) { // Create duplicate with unique name let uniqueName = imageStore.generateUniqueCopyName(baseName: finalName) if shouldUpdateUI { // Direct add behavior: immediate UI updates for file promise drops print("πŸ”₯ DEBUG: About to create duplicate with name: '\(uniqueName)'") print("πŸ”₯ DEBUG: Current image count BEFORE: \(imageStore.images.count)") let oldCount = imageStore.images.count imageStore.addImage(existingStashItem.nsImage, fileURL: nil, suggestedName: uniqueName, skipDuplicateCheck: true) print("πŸ”₯ DEBUG: Setting currentPreviewName IMMEDIATELY to: '\(uniqueName)'") self.currentPreviewName = uniqueName // Wait for the new item to be added, then update hoveredImageID DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { print("πŸ”₯ DEBUG: Async check - image count NOW: \(self.imageStore.images.count)") if self.imageStore.images.count > oldCount { if let newDuplicate = self.imageStore.images.last { print("πŸ”₯ MEGA FIX: Updating hoveredImageID to new duplicate: \(newDuplicate.id)") print("πŸ”₯ DEBUG: New duplicate name: '\(newDuplicate.fileURL?.deletingPathExtension().lastPathComponent ?? "unknown")'") self.hoveredImageID = newDuplicate.id } } } } else { // Handle drop behavior: different UI update pattern for regular drops let oldCount = imageStore.images.count imageStore.addImage(existingStashItem.nsImage, fileURL: nil, suggestedName: uniqueName, skipDuplicateCheck: true) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { if self.imageStore.images.count > oldCount { if let newDuplicate = self.imageStore.images.last { print("πŸ”₯ MEGA FIX: Updating hoveredImageID to new duplicate: \(newDuplicate.id) via handleDrop") self.hoveredImageID = newDuplicate.id self.currentPreviewName = uniqueName } } } } print("πŸ”₯ ULTRA MODE: Created internal stash duplicate with name: '\(uniqueName)'") return } } let fileExtension = fileURL.pathExtension // Generate unique filename var finalFilename = "\(finalName).\(fileExtension)" var counter = 1 while FileManager.default.fileExists(atPath: stashDir.appendingPathComponent(finalFilename).path) { finalFilename = "\(finalName) (\(counter)).\(fileExtension)" counter += 1 if counter > 100 { break } } let destinationURL = stashDir.appendingPathComponent(finalFilename) do { // Copy the file to stash directory try FileManager.default.copyItem(at: fileURL, to: destinationURL) print("βœ… File copied to stash: \(finalFilename) (preserved name: \(finalName))") // Load image and add to store if let nsImage = NSImage(contentsOf: destinationURL) { // πŸ”₯ ALWAYS skip duplicate check for external files to allow duplicates from BGR thumbnails imageStore.addImage(nsImage, fileURL: destinationURL, suggestedName: finalName, skipDuplicateCheck: true) print("βœ… Added copied file to stash store: \(finalName) (duplicate check skipped - allows BGR duplicates)") } } catch { print("❌ Failed to copy file to stash: \(error)") // Fallback: use NSImage loading if let nsImage = NSImage(contentsOf: fileURL) { // πŸ”₯ ALWAYS skip duplicate check for fallback external files too imageStore.addImage(nsImage, fileURL: nil, suggestedName: finalName, skipDuplicateCheck: true) print("🎯 Fallback: Added image with name '\(finalName)' (duplicate check skipped - allows BGR duplicates)") } } } private func handleDrop(providers: [NSItemProvider]) { print("πŸ” handleDrop called with \(providers.count) providers") for provider in providers { processDropProvider(provider) } } // MARK: - Private Helper Methods for handleDrop private func processDropProvider(_ provider: NSItemProvider) { print("πŸ” Provider: \(provider)") print("πŸ” Registered types: \(provider.registeredTypeIdentifiers)") // Try file URL types first if tryProcessFileURLProvider(provider) { return } // Fallback to NSImage if no file URL found processNSImageProvider(provider) } private func tryProcessFileURLProvider(_ provider: NSItemProvider) -> Bool { // Check for NSFilePromiseProvider metadata first if provider.hasItemConformingToTypeIdentifier("com.apple.NSFilePromiseItemMetaData") { print("πŸ” Found NSFilePromiseProvider metadata") processFilePromiseMetadata(provider) } // Try different file URL types let fileURLTypes = [ "NSFilenamesPboardType", // Oudere Finder drag "public.file-url", // Standaard file URL "com.apple.finder.noderef", // Finder node reference "com.apple.finder.node", // Finder node "public.url", // Algemene URL "CorePasteboardFlavorType 0x666C7574" // Oude legacy type ] for urlType in fileURLTypes { if provider.hasItemConformingToTypeIdentifier(urlType) { print("πŸ” Found file URL with type: \(urlType)") processFileURLType(provider, urlType: urlType) return true } } return false } private func processFilePromiseMetadata(_ provider: NSItemProvider) { provider.loadItem(forTypeIdentifier: "com.apple.NSFilePromiseItemMetaData", options: nil) { (item, error) in print("πŸ” NSFilePromiseProvider metadata: \(String(describing: item)), error: \(String(describing: error))") if let metadata = item as? [String: Any] { print("πŸ” Metadata content: \(metadata)") if let filename = metadata["filename"] as? String { print("πŸ” Found filename in metadata: \(filename)") } } } } private func processFileURLType(_ provider: NSItemProvider, urlType: String) { provider.loadItem(forTypeIdentifier: urlType, options: nil) { (item, error) in print("πŸ” File URL callback for \(urlType) - item type: \(type(of: item)), error: \(String(describing: error))") let (fileURL, filename) = self.extractFileURLAndName(from: item) if let url = fileURL, let name = filename { print("βœ… Successfully extracted file URL and name: \(name)") DispatchQueue.main.async { self.copyFileToStash(fileURL: url, preservedName: name, shouldUpdateUI: true) } } } } private func extractFileURLAndName(from item: Any?) -> (URL?, String?) { var fileURL: URL? = nil var filename: String? = nil if let url = item as? URL { print("πŸ”„ Direct URL: \(url.lastPathComponent)") fileURL = url filename = url.deletingPathExtension().lastPathComponent } else if let data = item as? Data { print("πŸ” Got Data (\(data.count) bytes), trying to decode...") (fileURL, filename) = extractFromData(data) } else if let string = item as? String { print("πŸ” Got string: '\(string)'") let url = URL(string: string) ?? URL(fileURLWithPath: string) print("πŸ”„ URL from string: \(url.lastPathComponent)") fileURL = url filename = url.deletingPathExtension().lastPathComponent } else if let array = item as? [String] { print("πŸ” Got string array: \(array)") if let firstPath = array.first { let url = URL(string: firstPath) ?? URL(fileURLWithPath: firstPath) print("πŸ”„ URL from array: \(url.lastPathComponent)") fileURL = url filename = url.deletingPathExtension().lastPathComponent } } else if let nsArray = item as? NSArray { print("πŸ” Got NSArray: \(nsArray)") if let firstPath = nsArray.firstObject as? String { let url = URL(string: firstPath) ?? URL(fileURLWithPath: firstPath) print("πŸ”„ URL from NSArray: \(url.lastPathComponent)") fileURL = url filename = url.deletingPathExtension().lastPathComponent } } return (fileURL, filename) } private func extractFromData(_ data: Data) -> (URL?, String?) { // Try as URL data if let url = URL(dataRepresentation: data, relativeTo: nil) { print("πŸ”„ Decoded URL from data: \(url.lastPathComponent)") return (url, url.deletingPathExtension().lastPathComponent) } else if let string = String(data: data, encoding: .utf8) { print("πŸ” Data as string: '\(string)'") let url = URL(string: string) ?? URL(fileURLWithPath: string) print("πŸ”„ Decoded URL from string: \(url.lastPathComponent)") return (url, url.deletingPathExtension().lastPathComponent) } return (nil, nil) } private func processNSImageProvider(_ provider: NSItemProvider) { guard provider.canLoadObject(ofClass: NSImage.self) else { print("Kan object niet laden als NSImage.") return } print("πŸ” Can load NSImage, checking for better name sources...") // Try to extract name from various sources let nameForStore = extractNameFromProvider(provider) _ = provider.loadObject(ofClass: NSImage.self) { image, error in if let nsImage = image as? NSImage { print("πŸ” Loaded NSImage successfully") print("πŸ” About to add to store with name: '\(nameForStore ?? "nil")'") DispatchQueue.main.async { // Skip duplicate check for external NSImage drops self.imageStore.addImage(nsImage, fileURL: nil, suggestedName: nameForStore, skipDuplicateCheck: true) } } else { if let error = error { print("Fout bij het laden van afbeelding: \(error.localizedDescription)") } else { print("Kon afbeelding niet laden, onbekende fout.") } } } } private func extractNameFromProvider(_ provider: NSItemProvider) -> String? { // 1. Provider suggested name if let suggestedNameFromProvider = provider.suggestedName, !suggestedNameFromProvider.isEmpty { let name = (suggestedNameFromProvider as NSString).deletingPathExtension print("πŸ” Provider suggested name: \(suggestedNameFromProvider) -> \(name)") return name } // 2. Try via pasteboard types for typeId in provider.registeredTypeIdentifiers { print("πŸ” Examining type identifier: \(typeId)") // Sometimes name is in the type identifier if typeId.contains(".") && !typeId.hasPrefix("public.") && !typeId.hasPrefix("com.apple") { if let extractedName = typeId.components(separatedBy: ".").last, extractedName.count > 3 && extractedName.count < 50 { print("πŸ” Extracted name from type identifier: \(extractedName)") return extractedName } } } return nil } // NIEUW: Helper functie om file URL te verwerken private func processFileURL(_ url: URL) { // Check of het een image bestand is let imageExtensions = ["png", "jpg", "jpeg", "gif", "bmp", "tiff", "webp", "heic"] let fileExtension = url.pathExtension.lowercased() if imageExtensions.contains(fileExtension) { DispatchQueue.main.async { let filename = url.deletingPathExtension().lastPathComponent self.copyFileToStash(fileURL: url, preservedName: filename) } } else { print("⚠️ Not an image file: \(fileExtension)") } } // AANGEPAST: Kopieer origineel bestand naar stash directory met preserved name // πŸ”₯ NIEUW: Drop Zone fade timer functie private func startDropZoneFadeTimer() { print("⏰ Drop Zone: startDropZoneFadeTimer() CALLED! dropZoneOpacity: \(dropZoneOpacity)") // Reset als er al een timer loopt dropZoneFadeTimer?.invalidate() // Na 3 seconden starten met uitfaden over 10 seconden dropZoneFadeTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in print("πŸŒ… Drop Zone: Starting 10-second fade out...") // Gebruik SwiftUI animatie voor smooth fade over 10 seconden withAnimation(.linear(duration: 10.0)) { dropZoneOpacity = 0.0 } // Log completion DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) { print("πŸŒ™ Drop Zone: Fade out completed (fully transparent)") } } print("⏰ Drop Zone: 3-second delay timer started, will fade over 10 seconds") } // 🎯 NIEUW: Test shake animation functie - GEFIXT MET CAKeyframeAnimation! private func testShakeAnimation() { guard let window = hostingWindow else { print("🚨 Shake test: No hosting window available!") return } print("πŸ”₯ MEGA SHAKE: Starting CAKeyframeAnimation shake!") // PROVEN WORKING TECHNIQUE from Eric Dolecki blog let numberOfShakes = 4 let durationOfShake = 0.3 let vigourOfShake: CGFloat = 0.03 // Percentage of window width let frame = window.frame print("πŸ”₯ MEGA SHAKE: Parameters - shakes: \(numberOfShakes), duration: \(durationOfShake)s, vigour: \(vigourOfShake)") print("πŸ”₯ MEGA SHAKE: Window frame: \(frame)") // Create CAKeyframeAnimation let shakeAnimation = CAKeyframeAnimation() // Create shake path let shakePath = CGMutablePath() shakePath.move(to: CGPoint(x: frame.minX, y: frame.minY)) for _ in 0..= previewScreen.visibleFrame.minY { previewY = stashWindowFrame.minY - (spacing + 25) - previewHeight print("πŸ”½ VULCANO: Positioning preview BELOW stash window with EXTRA spacing!") } // Fallback: rechtsboven op het scherm else { previewY = previewScreen.visibleFrame.maxY - previewHeight - 20 print("πŸŒ‹ VULCANO: No space above/below - fallback to top of screen") } } } else { // ◀️▢️ FIXED COLUMNS = VERTICALE STRIP β†’ PREVIEW LINKS/RECHTS! ▢️◀️ print("πŸŒ‹ VULCANO: Fixed Columns mode - positioning preview LEFT/RIGHT of stash") previewY = stashWindowFrame.midY - previewHeight / 2 // πŸ”₯ MEGA GRID FORCED POSITIONING FOR COLUMNS! πŸ”₯ if gridColumn == 1 { // Left half: ALWAYS position preview RIGHT previewX = stashWindowFrame.maxX + spacing print("πŸ”₯ MEGA GRID: Column 1 (LEFT HALF) - FORCING preview RIGHT!") } else { // Right half: ALWAYS position preview LEFT previewX = stashWindowFrame.minX - (spacing + 15) - previewWidth print("πŸ”₯ MEGA GRID: Column 2 (RIGHT HALF) - FORCING preview LEFT!") } } let finalPreviewOrigin = NSPoint( x: max(previewScreen.visibleFrame.minX, min(previewX, previewScreen.visibleFrame.maxX - previewWidth)), y: max(previewScreen.visibleFrame.minY, min(previewY, previewScreen.visibleFrame.maxY - previewHeight)) ) print("πŸŒ‹ VULCANO: Final preview position: \(finalPreviewOrigin)") // --- VERBETERDE logica voor het creΓ«ren/updaten van het venster --- if previewWindowController == nil { print("πŸ–ΌοΈ IntegratedGalleryView: previewWindowController is nil. Creating new preview window.") createNewPreviewWindow(imageToDisplayInPreview, at: finalPreviewOrigin, size: NSSize(width: previewWidth, height: previewHeight), screen: previewScreen) } else { print("πŸ–ΌοΈ IntegratedGalleryView: previewWindowController exists. Updating existing window.") updateExistingPreviewWindow(imageToDisplayInPreview, at: finalPreviewOrigin, size: NSSize(width: previewWidth, height: previewHeight)) } } else { print("πŸ–ΌοΈ IntegratedGalleryView: handlePreviewWindowState - Condition NOT met to show preview OR shouldHide. Closing.") // ⏰ STOP TIMER FORCE REFRESH! stopPreviewNameRefreshTimer() // NIEUW: Minder aggressive sluiten if let wc = self.previewWindowController, let pWindow = wc.window, pWindow.isVisible { NSAnimationContext.runAnimationGroup({ context in context.duration = 0.15 // Kortere fade out pWindow.animator().alphaValue = 0 }, completionHandler: { self.closePreviewWindow() }) } else { closePreviewWindow() } } } // NIEUW: Separate functie voor nieuwe preview window private func createNewPreviewWindow(_ image: NSImage, at origin: NSPoint, size: NSSize, screen: NSScreen) { // πŸ”₯πŸ’Ž MEGA TITANIUM GLASS EFFECT PREVIEW! πŸ’ŽπŸ”₯ let previewContentSwiftUIView = VStack(spacing: 4) { Image(nsImage: image) .resizable() .scaledToFit() .cornerRadius(8) .clipped() .shadow(radius: 3, y: 2) // πŸš€πŸ’₯ TURBO INSTANT NAAM LABEL! πŸ’₯πŸš€ Text(currentPreviewName) .font(.system(size: 11, weight: .medium, design: .rounded)) .foregroundColor(Color.adaptivePrimaryText) // πŸ”₯ ADAPTIVE text voor light/dark mode .opacity(0.9) // πŸ”₯ Hoge opacity voor zichtbaarheid .lineLimit(1) .truncationMode(.middle) .padding(.horizontal, 8) .onAppear { print("πŸ”₯ ULTRA DEBUG: Preview text appeared with INSTANT name: '\(currentPreviewName)'") } } .padding(8) // πŸ”₯ EXTRA PADDING voor schaduw ruimte .background( ZStack { // πŸ”₯πŸ’Ž MEGA GLASS EFFECT ZOALS STASH! πŸ’ŽπŸ”₯ VisualEffectBackground(material: .hudWindow, blending: .behindWindow, alpha: 0.95) .cornerRadius(12) // πŸ”₯ Extra glas laag voor diepte VisualEffectBackground(material: .popover, blending: .withinWindow, alpha: 0.3) .cornerRadius(12) } ) .cornerRadius(12) .shadow(color: .black.opacity(0.4), radius: 8, x: 0, y: 3) // πŸ”₯ Zachte schaduw die past in window .padding(16) // πŸ”₯ CRITICAL: Extra padding voor schaduw ruimte let hostingController = NSHostingController(rootView: previewContentSwiftUIView) let previewWindow = NSWindow(contentViewController: hostingController) // πŸ”₯ MEGA FIX: Extra grote window size voor schaduw ruimte (shadow radius 8 + padding 16 = 24px extra aan alle kanten) let shadowPadding: CGFloat = 32 // Extra ruimte voor schaduw en padding let previewSize = CGSize( width: size.width + shadowPadding + 20, // Original + shadow space + name space height: size.height + shadowPadding + 40 // Original + shadow space + name space ) let adjustedOrigin = CGPoint( x: origin.x - shadowPadding/2, // Compenseer voor shadow padding y: origin.y - shadowPadding/2 - 10 // Compenseer voor shadow padding + 10px hoger ) previewWindow.setFrame(NSRect(origin: adjustedOrigin, size: previewSize), display: false) previewWindow.styleMask = [.borderless] // πŸ”₯ BLIJFT borderless previewWindow.backgroundColor = NSColor.clear // πŸ”₯ Volledig transparant previewWindow.isOpaque = false // πŸ”₯ Niet opaque previewWindow.hasShadow = false // πŸ”₯ NATIVE window shadow UIT - we gebruiken SwiftUI shadow previewWindow.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow))) previewWindow.ignoresMouseEvents = true let windowController = NSWindowController(window: previewWindow) self.previewWindowController = windowController previewWindow.orderFront(nil) previewWindow.alphaValue = 1.0 print("πŸ–ΌοΈ IntegratedGalleryView: Positioned NEW SHADOW-FRIENDLY stash hover preview at (\(adjustedOrigin.x), \(adjustedOrigin.y)) with size \(previewSize)") print("πŸ–ΌοΈ IntegratedGalleryView: Showing new BORDERLESS preview window with embedded shadows") } // ULTRA HELPER FUNCTIE: Real-time naam lookup private func getCurrentHoveredImageName() -> String { if let hoveredID = hoveredImageID, let hoveredItem = imageStore.images.first(where: { $0.id == hoveredID }) { let name = hoveredItem.fileURL?.deletingPathExtension().lastPathComponent ?? "Unknown" print("πŸ”₯ ULTRA DEBUG: getCurrentHoveredImageName() -> '\(name)' for ID \(hoveredID)") return name } print("πŸ”₯ ULTRA DEBUG: getCurrentHoveredImageName() -> 'Unknown' (no hovered ID)") return "Unknown" } // NIEUW: Separate functie voor update van bestaande preview window private func updateExistingPreviewWindow(_ image: NSImage, at origin: NSPoint, size: NSSize) { guard let wc = self.previewWindowController, let pWindow = wc.window else { print("πŸ–ΌοΈ IntegratedGalleryView: ERROR - previewWindowController or its window is nil in update path.") return } print("πŸ”₯ ULTRA DEBUG: updateExistingPreviewWindow - INSTANT name: '\(currentPreviewName)'") // πŸ”₯πŸ’Ž MEGA TITANIUM GLASS EFFECT UPDATE! πŸ’ŽπŸ”₯ let updatedPreviewContentSwiftUIView = VStack(spacing: 4) { Image(nsImage: image) .resizable() .scaledToFit() .cornerRadius(8) .clipped() .shadow(radius: 3, y: 2) // πŸš€πŸ’₯ TURBO INSTANT NAAM LABEL! πŸ’₯πŸš€ Text(currentPreviewName) .font(.system(size: 11, weight: .medium, design: .rounded)) .foregroundColor(Color.adaptivePrimaryText) // πŸ”₯ ADAPTIVE text voor light/dark mode .opacity(0.9) // πŸ”₯ Hoge opacity voor zichtbaarheid .lineLimit(1) .truncationMode(.middle) .padding(.horizontal, 8) .onAppear { print("πŸ”₯ ULTRA DEBUG: Updated preview text with INSTANT name: '\(currentPreviewName)'") } } .padding(8) // πŸ”₯ EXTRA PADDING voor schaduw ruimte .background( ZStack { // πŸ”₯πŸ’Ž MEGA GLASS EFFECT ZOALS STASH! πŸ’ŽπŸ”₯ VisualEffectBackground(material: .hudWindow, blending: .behindWindow, alpha: 0.95) .cornerRadius(12) // πŸ”₯ Extra glas laag voor diepte VisualEffectBackground(material: .popover, blending: .withinWindow, alpha: 0.3) .cornerRadius(12) } ) .cornerRadius(12) .shadow(color: .black.opacity(0.4), radius: 8, x: 0, y: 3) // πŸ”₯ Zachte schaduw die past in window .padding(16) // πŸ”₯ CRITICAL: Extra padding voor schaduw ruimte let updatedHostingController = NSHostingController(rootView: updatedPreviewContentSwiftUIView) pWindow.contentViewController = updatedHostingController // πŸ”₯πŸ’Ž MEGA DYNAMIC SIZE UPDATE! πŸ’ŽπŸ”₯ let originalSize = image.size let setting = SettingsManager.shared.stashPreviewSize // πŸ”₯ MEGA INTELLIGENTE SIZE CALCULATION! πŸ”₯ let targetSizes: [StashPreviewSize: CGSize] = [ .xSmall: CGSize(width: 150, height: 112), .small: CGSize(width: 240, height: 180), .medium: CGSize(width: 360, height: 270), .large: CGSize(width: 480, height: 360), .xLarge: CGSize(width: 600, height: 450) ] let targetSize = targetSizes[setting] ?? targetSizes[.medium]! let widthScale = targetSize.width / originalSize.width let heightScale = targetSize.height / originalSize.height let scale = min(widthScale, heightScale) var dynamicWidth = originalSize.width * scale var dynamicHeight = originalSize.height * scale // πŸ”₯ ABSOLUTE MIN/MAX LIMIETEN! πŸ”₯ dynamicWidth = max(80, min(dynamicWidth, 1000)) dynamicHeight = max(60, min(dynamicHeight, 750)) // πŸ”₯ MEGA FIX: Update position and size with shadow-friendly dimensions let shadowPadding: CGFloat = 32 // Extra ruimte voor schaduw en padding let previewSize = CGSize( width: dynamicWidth + shadowPadding + 20, // Original + shadow space + name space height: dynamicHeight + shadowPadding + 40 // Original + shadow space + name space ) let adjustedOrigin = CGPoint( x: origin.x - shadowPadding/2, // Compenseer voor shadow padding y: origin.y - shadowPadding/2 - 10 // Compenseer voor shadow padding + 10px hoger ) pWindow.setFrame(NSRect(origin: adjustedOrigin, size: previewSize), display: true) print("πŸ–ΌοΈ IntegratedGalleryView: Positioned UPDATED SHADOW-FRIENDLY stash hover preview at (\(adjustedOrigin.x), \(adjustedOrigin.y)) with dynamic size \(dynamicWidth)x\(dynamicHeight)") } private func closePreviewWindow() { stopPreviewNameRefreshTimer() // ⏰ Stop timer when closing previewWindowController?.window?.orderOut(nil) previewWindowController?.close() previewWindowController = nil } // ⏰ TIMER FORCE REFRESH FUNCTIONS private func startPreviewNameRefreshTimer() { // Stop existing timer first stopPreviewNameRefreshTimer() print("⏰ TIMER: Starting 0.2s refresh timer for preview name") previewNameRefreshTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { _ in DispatchQueue.main.async { [self] in self.forceRefreshPreviewName() } } } private func stopPreviewNameRefreshTimer() { if previewNameRefreshTimer != nil { print("⏰ TIMER: Stopping refresh timer") previewNameRefreshTimer?.invalidate() previewNameRefreshTimer = nil } } private func forceRefreshPreviewName() { guard showPreview else { print("⏰ TIMER DEBUG: Not showing preview, skipping refresh") return } print("⏰ TIMER DEBUG: forceRefreshPreviewName called") print("⏰ TIMER DEBUG: hoveredImageID: \(hoveredImageID?.uuidString ?? "nil")") print("⏰ TIMER DEBUG: isTargeted: \(isTargeted)") print("⏰ TIMER DEBUG: imageStore.images.count: \(imageStore.images.count)") // πŸ”₯ MEGA ULTRA FIX: Check if we're hovering over the dropzone // If the hoveredImageID is nil but we're showing preview, it means we're hovering over dropzone // In that case, show the LAST item (newest duplicate) let itemToShow: IdentifiableImage? if let hoveredID = hoveredImageID { // Normal case: hovering over a specific thumbnail itemToShow = imageStore.images.first(where: { $0.id == hoveredID }) print("⏰ TIMER: Using hovered item with ID: \(hoveredID)") } else if isTargeted && !imageStore.images.isEmpty { // πŸ”₯ SPECIAL CASE: Hovering over dropzone - show LAST item (newest) itemToShow = imageStore.images.last print("⏰ TIMER: DROPZONE HOVER DETECTED - Using LAST item (newest duplicate)") } else { itemToShow = nil print("⏰ TIMER DEBUG: No item to show") } // Get current item name if let item = itemToShow { let newName = item.fileURL?.deletingPathExtension().lastPathComponent ?? "Unknown" print("⏰ TIMER DEBUG: Item found, name: '\(newName)', current preview name: '\(currentPreviewName)'") // Only update if name actually changed to avoid unnecessary updates if newName != currentPreviewName { currentPreviewName = newName print("⏰ TIMER: Force refreshed preview name to '\(newName)'") // Force update the preview window if it exists if previewWindowController != nil, let image = imageForPreview { print("⏰ TIMER DEBUG: Updating preview window with new name") let stashWindowFrame = hostingWindow?.frame ?? NSRect.zero let previewScreen = hostingWindow?.screen ?? NSScreen.main ?? NSScreen.screens.first! // πŸ”₯πŸ’Ž MEGA DYNAMIC SIZE FOR TIMER UPDATE TOO! πŸ’ŽπŸ”₯ let originalSize = image.size let setting = SettingsManager.shared.stashPreviewSize // πŸ”₯ MEGA INTELLIGENTE SIZE CALCULATION! πŸ”₯ let targetSizes: [StashPreviewSize: CGSize] = [ .xSmall: CGSize(width: 150, height: 112), .small: CGSize(width: 240, height: 180), .medium: CGSize(width: 360, height: 270), .large: CGSize(width: 480, height: 360), .xLarge: CGSize(width: 600, height: 450) ] let targetSize = targetSizes[setting] ?? targetSizes[.medium]! let widthScale = targetSize.width / originalSize.width let heightScale = targetSize.height / originalSize.height let scale = min(widthScale, heightScale) var previewWidth = originalSize.width * scale var previewHeight = originalSize.height * scale // πŸ”₯ ABSOLUTE MIN/MAX LIMIETEN! πŸ”₯ previewWidth = max(80, min(previewWidth, 1000)) previewHeight = max(60, min(previewHeight, 750)) let spacing: CGFloat = 10 var previewX: CGFloat let previewY = stashWindowFrame.midY - previewHeight / 2 if stashWindowFrame.maxX + spacing + previewWidth <= previewScreen.visibleFrame.maxX { previewX = stashWindowFrame.maxX + spacing } else if stashWindowFrame.minX - (spacing + 15) - previewWidth >= previewScreen.visibleFrame.minX { previewX = stashWindowFrame.minX - (spacing + 15) - previewWidth } else { previewX = previewScreen.visibleFrame.maxX - previewWidth - 20 } let finalPreviewOrigin = NSPoint(x: previewX, y: max(previewScreen.visibleFrame.minY, min(previewY, previewScreen.visibleFrame.maxY - previewHeight))) updateExistingPreviewWindow(image, at: finalPreviewOrigin, size: NSSize(width: previewWidth, height: previewHeight)) } else { print("⏰ TIMER DEBUG: No preview window controller to update") } } else { print("⏰ TIMER DEBUG: Name unchanged, skipping update") } } } // MARK: - Save dropped thumbnail private func saveDroppedURL(_ sourceURL: URL, fileName: String) { guard let destFolder = SettingsManager.shared.screenshotFolder, !destFolder.isEmpty else { print("❌ Save failed: default folder not set") return } let destURL = URL(fileURLWithPath: destFolder).appendingPathComponent(fileName) if FileManager.default.fileExists(atPath: destURL.path) { print("⚠️ File already exists, skipping save") return } do { try FileManager.default.copyItem(at: sourceURL, to: destURL) print("βœ… Saved stash thumbnail to \(destURL.path)") } catch { print("❌ Error saving stash thumbnail: \(error)") } } // NIEUW: Helper functie om gecachte delegate te krijgen private func getDragDelegate(for item: IdentifiableImage) -> StashDragDelegate { if let cachedDelegate = dragDelegateCache[item.id] { return cachedDelegate } else { let newDelegate = StashDragDelegate(imageItem: item, imageStore: imageStore) dragDelegateCache[item.id] = newDelegate // Set the stash grid manager reference ONCE when creating the delegate // For now, we'll set it later when the actual drag starts since the stash grid manager // might not be created yet when the delegate is first created return newDelegate } } // ULTRA REACTIVE: Computed property die automatisch update wanneer hoveredImageID verandert private var currentHoveredImageName: String { guard let hoveredID = hoveredImageID, let hoveredItem = imageStore.images.first(where: { $0.id == hoveredID }) else { return "Unknown" } let name = hoveredItem.fileURL?.deletingPathExtension().lastPathComponent ?? "Unknown" print("πŸ”₯ ULTRA DEBUG: currentHoveredImageName computed - hoveredID: \(hoveredID), name: '\(name)'") return name } } // MARK: - StashDragDelegate Class class StashDragDelegate: StashDraggableImageViewDelegate { let imageItem: IdentifiableImage let imageStore: GalleryImageStore var stashGridActionDelegate: StashGridActionDelegate? // Strong reference to prevent deallocation during operations init(imageItem: IdentifiableImage, imageStore: GalleryImageStore) { self.imageItem = imageItem self.imageStore = imageStore // Create a completely independent StashGridActionDelegate // BELANGRIJK: Houd sterke reference om deallocation tijdens rename te voorkomen self.stashGridActionDelegate = StashGridActionDelegate(imageStore: imageStore) // NIEUW: Set the presenting window for proper dialog positioning DispatchQueue.main.async { [weak self] in // Find the stash window for window in NSApp.windows { if window.title.contains("Stash") || window.contentView?.identifier?.rawValue.contains("stash") == true { self?.stashGridActionDelegate?.presentingWindow = window break } } // Fallback: use any visible window if self?.stashGridActionDelegate?.presentingWindow == nil { self?.stashGridActionDelegate?.presentingWindow = NSApp.windows.first { $0.isVisible } } } } // NIEUW: Set stash grid manager reference for proper grid frame positioning func setStashGridManager(_ manager: StashGridManager?) { // Only set if not already set to avoid excessive logging guard stashGridActionDelegate?.stashGridManager !== manager else { return } stashGridActionDelegate?.stashGridManager = manager // KRITIEKE REPARATIE: Stel stashGridActionDelegate in als delegate van de manager if let manager = manager, let actionDelegate = stashGridActionDelegate { manager.delegate = actionDelegate } } func stashImageDidStartDrag(imageURL: URL, from view: StashDraggableNSImageView) { print("🎯 StashDragDelegate: Drag started for \(imageURL.lastPathComponent)") print("🎯 StashDragDelegate: Grid delegate is \(stashGridActionDelegate != nil ? "SET" : "NIL")") } func stashImageDragDidEnd(imageURL: URL, operation: NSDragOperation, from view: StashDraggableNSImageView) { print("🎯 StashDragDelegate: Drag ended for \(imageURL.lastPathComponent) with operation \(operation.rawValue)") print("🎯 StashDragDelegate: Grid delegate is \(stashGridActionDelegate != nil ? "SET" : "NIL") after drag end") } } // MARK: - Stash Grid Action Delegate class StashGridActionDelegate: StashGridDelegate, RenameActionHandlerDelegate { let imageStore: GalleryImageStore weak var presentingWindow: NSWindow? private var renameActionHandler: RenameActionHandler? // NIEUW: Reference naar stash grid manager voor correcte grid frame weak var stashGridManager: StashGridManager? init(imageStore: GalleryImageStore) { self.imageStore = imageStore self.renameActionHandler = RenameActionHandler(delegate: self) } func stashGridDidDropImage(at cellIndex: Int, stashItem: IdentifiableImage, imageURL: URL) { print("βœ… StashGridActionDelegate: Independent action for cell \(cellIndex), stash item \(stashItem.id)") // πŸ”„ NEW: Set stash grid action flag for DraggableImageView post-action handling if let appDelegate = NSApp.delegate as? ScreenshotApp { appDelegate.didStashGridHandleDrop = true print("πŸ”„ Set didStashGridHandleDrop = true for stash action") } // CRITICAL FIX: Use dynamic action mapping instead of hardcoded cellIndex let settings = SettingsManager.shared // Get the active actions in the same order as StashGridWindow creates them var activeActionTypes: [ActionType] = [] for actionType in settings.actionOrder { let isEnabled: Bool switch actionType { case .rename: isEnabled = settings.isRenameActionEnabled case .stash: isEnabled = settings.isStashActionEnabled case .ocr: isEnabled = settings.isOCRActionEnabled case .clipboard: isEnabled = settings.isClipboardActionEnabled case .backgroundRemove: isEnabled = settings.isBackgroundRemoveActionEnabled case .cancel: isEnabled = settings.isCancelActionEnabled case .remove: isEnabled = settings.isRemoveActionEnabled } if isEnabled { activeActionTypes.append(actionType) } } // Map cellIndex to actual ActionType guard cellIndex >= 0 && cellIndex < activeActionTypes.count else { print("⚠️ StashGridActionDelegate: Invalid cell index \(cellIndex), available actions: \(activeActionTypes.count)") return } let actionType = activeActionTypes[cellIndex] print("🎯 StashGridActionDelegate: Cell \(cellIndex) maps to action: \(actionType)") // Execute action based on ActionType instead of cellIndex switch actionType { case .rename: handleStashRename(stashItem: stashItem, imageURL: imageURL) case .stash: handleStashDuplicate(stashItem: stashItem, imageURL: imageURL) // Stash = duplicate for stash items case .clipboard: handleStashClipboard(stashItem: stashItem, imageURL: imageURL) case .ocr: handleStashOCR(stashItem: stashItem, imageURL: imageURL) case .backgroundRemove: handleStashBackgroundRemove(stashItem: stashItem, imageURL: imageURL) case .remove: handleStashRemove(stashItem: stashItem, imageURL: imageURL) case .cancel: handleStashClose() } } // MARK: - Independent Stash Actions private func handleStashRename(stashItem: IdentifiableImage, imageURL: URL) { print("πŸ“ StashGridActionDelegate: Using real RenameActionHandler for stash item \(stashItem.id)") // Store current stash item for completion handling self.currentStashItem = stashItem // Use the real RenameActionHandler just like the main thumbnail renameActionHandler?.promptAndRename(originalURL: imageURL) { [self] response in print("πŸ” DEBUG: Stash rename response: \(response), successful: \(response != .cancel)") // Handle Save to Folder action (second button) if response == .continue { print("πŸ” DEBUG: Processing Save to Folder for stash item") if let updatedURL = self.currentStashItem?.fileURL ?? imageURL as URL? { self.saveToFolder(fileURL: updatedURL) { success in print("πŸ” DEBUG: Save to Folder result: \(success)") } } } // Reset current stash item ALLEEN als de response succesvol was if response != .cancel { print("πŸ” DEBUG: Resetting currentStashItem after successful rename") } self.currentStashItem = nil } } // Store current stash item for rename completion private var currentStashItem: IdentifiableImage? // MARK: - RenameActionHandlerDelegate Implementation func getScreenshotFolder() -> String? { return SettingsManager.shared.screenshotFolder } func renameActionHandler(_ handler: RenameActionHandler, didRenameFileFrom oldURL: URL, to newURL: URL) { print("βœ… Stash item renamed from \(oldURL.lastPathComponent) to \(newURL.lastPathComponent)") // Update the current stash item in GalleryImageStore guard let stashItem = currentStashItem else { print("⚠️ No current stash item to update after rename") return } DispatchQueue.main.async { [weak self] in guard let self = self else { return } if let index = self.imageStore.images.firstIndex(where: { $0.id == stashItem.id }) { let newName = newURL.deletingPathExtension().lastPathComponent self.imageStore.images[index].customName = newName self.imageStore.images[index].fileURL = newURL print("βœ… Updated stash item in store with customName: '\(newName)'") // NIEUW: Force UI update door de hele array te 'verversen' self.imageStore.objectWillChange.send() } } } // NIEUW: Implement Save to Folder functionality for stash items func saveToFolder(fileURL: URL, completion: @escaping (Bool) -> Void) { guard let destinationFolder = SettingsManager.shared.screenshotFolder, !destinationFolder.isEmpty else { print("❌ No screenshot folder set for stash save") DispatchQueue.main.async { let alert = NSAlert() alert.messageText = "Folder Not Set" alert.informativeText = "Please set a default save folder in Settings." alert.addButton(withTitle: "OK") alert.runModal() completion(false) } return } let destinationURL = URL(fileURLWithPath: destinationFolder).appendingPathComponent(fileURL.lastPathComponent) do { // Check if file already exists if FileManager.default.fileExists(atPath: destinationURL.path) { print("⚠️ File already exists at destination: \(destinationURL.lastPathComponent)") DispatchQueue.main.async { let alert = NSAlert() alert.messageText = "File Exists" alert.informativeText = "A file with the name '\(destinationURL.lastPathComponent)' already exists in the destination folder." alert.addButton(withTitle: "OK") alert.runModal() completion(false) } return } // Copy file to destination try FileManager.default.copyItem(at: fileURL, to: destinationURL) print("βœ… Stash item saved to folder: \(destinationURL.path)") DispatchQueue.main.async { completion(true) } } catch { print("❌ Failed to save stash item to folder: \(error)") DispatchQueue.main.async { let alert = NSAlert() alert.messageText = "Save Failed" alert.informativeText = "Could not save the file: \(error.localizedDescription)" alert.addButton(withTitle: "OK") alert.runModal() completion(false) } } } func findFilenameLabel(in window: NSWindow?) -> NSTextField? { // For stash items, we don't have a filename label like the main preview return nil } func setTempFileURL(_ url: URL?) { // For stash items, update the current item's fileURL guard let stashItem = currentStashItem, let newURL = url else { return } DispatchQueue.main.async { [weak self] in guard let self = self else { return } if let index = self.imageStore.images.firstIndex(where: { $0.id == stashItem.id }) { self.imageStore.images[index].fileURL = newURL } } } func getActivePreviewWindow() -> NSWindow? { return presentingWindow } func closePreviewWithAnimation(immediate: Bool, preserveTempFile: Bool = false) { // For stash, we don't close the entire stash window during rename print("πŸ” Stash: closePreviewWithAnimation called (no action needed)") } func getGridWindowFrame() -> NSRect? { // FIXED: Return the actual stash grid frame instead of entire stash window if let stashGridFrame = stashGridManager?.gridWindow?.frame { print("πŸ” DEBUG: Using actual stash grid frame: \(stashGridFrame)") return stashGridFrame } else { print("πŸ” DEBUG: No stash grid window available, using presenting window frame") return presentingWindow?.frame } } func hideGrid() { print("πŸ” Stash: hideGrid called") // The stash grid is handled by StashGridManager } func disableGridMonitoring() { print("πŸ” Stash: disableGridMonitoring called") // Stash grid doesn't have proximity monitoring } func enableGridMonitoring() { print("πŸ” Stash: enableGridMonitoring called") // Stash grid doesn't have proximity monitoring } private func handleStashDuplicate(stashItem: IdentifiableImage, imageURL: URL) { print("βž• StashGridActionDelegate: Independent duplicate for stash item \(stashItem.id)") // Create a new stash item (duplicate) let newStashItem = IdentifiableImage( nsImage: stashItem.nsImage, fileURL: nil, // Will get permanent URL when added customName: stashItem.customName ) DispatchQueue.main.async { [weak self] in guard let self = self else { return } let baseName = stashItem.customName ?? "Image" let uniqueName = self.imageStore.generateUniqueCopyName(baseName: baseName) self.imageStore.addImage(newStashItem.nsImage, fileURL: nil, suggestedName: uniqueName, skipDuplicateCheck: true) print("βœ… Created duplicate stash item with name: '\(uniqueName)'") // πŸ”₯ ULTRA FIX: Show balloon feedback like hoofdgrid if let gridFrame = self.getGridWindowFrame() { let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Duplicated!") panel.show(aroundGridFrame: gridFrame, text: "Duplicated!", autoCloseAfter: nil) DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { panel.closeWithAnimation(completion: nil) } } } } private func handleStashClipboard(stashItem: IdentifiableImage, imageURL: URL) { print("πŸ“‹ StashGridActionDelegate: Independent clipboard for stash item \(stashItem.id)") guard let image = NSImage(contentsOf: imageURL) else { print("❌ Could not load stash image for clipboard") // πŸ”₯ ULTRA FIX: Show error balloon if let gridFrame = getGridWindowFrame() { let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Error") panel.show(aroundGridFrame: gridFrame, text: "Image load failed for Clipboard", autoCloseAfter: 2.0) } return } let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.writeObjects([image]) print("βœ… Stash image copied to clipboard") // πŸ”₯ ULTRA FIX: Show success balloon like hoofdgrid if let gridFrame = getGridWindowFrame() { let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Copied!") panel.show(aroundGridFrame: gridFrame, text: "Copied to clipboard!", autoCloseAfter: nil) DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { panel.closeWithAnimation(completion: nil) } } } private func handleStashOCR(stashItem: IdentifiableImage, imageURL: URL) { print("🧐 StashGridActionDelegate: Independent OCR for stash item \(stashItem.id)") guard let nsImage = NSImage(contentsOf: imageURL), let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil) else { print("❌ Could not load stash image for OCR") // πŸ”₯ ULTRA FIX: Show error balloon if let gridFrame = getGridWindowFrame() { let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Error") panel.show(aroundGridFrame: gridFrame, text: "Image load failed for OCR", autoCloseAfter: 2.0) } return } // πŸ”₯ ULTRA FIX: Show processing balloon like hoofdgrid let processingPanel: FeedbackBubblePanel? if let gridFrame = getGridWindowFrame() { processingPanel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Processing...") processingPanel?.show(aroundGridFrame: gridFrame, text: "Processing...", autoCloseAfter: nil) } else { processingPanel = nil } let request = VNRecognizeTextRequest { request, error in DispatchQueue.main.async { [weak self] in guard let self = self else { return } // πŸ”₯ ULTRA FIX: Close processing balloon first processingPanel?.closeWithAnimation(completion: nil) var message = "" var ocrDidFindText = false if let error = error { print("❌ OCR failed: \(error.localizedDescription)") message = "OCR error: \(error.localizedDescription)" } else { guard let observations = request.results as? [VNRecognizedTextObservation] else { print("❌ No OCR results") message = "No text found" // πŸ”₯ ULTRA FIX: Show result balloon if let gridFrame = self.getGridWindowFrame() { let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: message) panel.show(aroundGridFrame: gridFrame, text: message, autoCloseAfter: 1.8, onAutoCloseCompletion: nil) } return } let recognizedText = observations.compactMap { observation in observation.topCandidates(1).first?.string }.joined(separator: "\n") if recognizedText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { print("πŸ“ OCR found no readable text") message = "No text found" } else { let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.setString(recognizedText, forType: .string) print("βœ… OCR text copied to clipboard: \(recognizedText.prefix(50))...") message = "Text copied to clipboard!" ocrDidFindText = true } } // πŸ”₯ ULTRA FIX: Always show result balloon if let gridFrame = self.getGridWindowFrame() { let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: message) panel.show(aroundGridFrame: gridFrame, text: message, autoCloseAfter: 1.8, onAutoCloseCompletion: nil) } } } request.recognitionLevel = VNRequestTextRecognitionLevel.accurate request.usesLanguageCorrection = true let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) DispatchQueue.global(qos: .userInitiated).async { do { try handler.perform([request]) } catch { print("❌ OCR handler failed: \(error.localizedDescription)") DispatchQueue.main.async { [weak self] in guard let self = self else { return } // πŸ”₯ ULTRA FIX: Close processing balloon first processingPanel?.closeWithAnimation(completion: nil) if let gridFrame = self.getGridWindowFrame() { let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "OCR failed") panel.show(aroundGridFrame: gridFrame, text: "OCR failed", autoCloseAfter: 2.0) } } } } } private func handleStashBackgroundRemove(stashItem: IdentifiableImage, imageURL: URL) { print("πŸͺ„ StashGridActionDelegate: BGR thumbnail workflow for stash item \(stashItem.id)") guard let image = NSImage(contentsOf: imageURL) else { print("❌ Could not load stash image for background removal") return } // 🎨 NEW: Use new BGR thumbnail workflow instead of old BGR window if let appDelegate = NSApp.delegate as? ScreenshotApp { print("🎨 Starting BGR thumbnail workflow for stash item") appDelegate.showBackgroundRemovalThumbnail(with: image, originalURL: imageURL) print("βœ… Started BGR thumbnail workflow for stash item") } else { print("❌ Could not get app delegate for BGR thumbnail workflow") } } private func handleStashRemove(stashItem: IdentifiableImage, imageURL: URL) { print("πŸ—‘οΈ StashGridActionDelegate: Independent remove for stash item \(stashItem.id)") // Remove from file system do { try FileManager.default.removeItem(at: imageURL) // Remove from GalleryImageStore DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.imageStore.images.removeAll { $0.id == stashItem.id } print("βœ… Removed stash item from store") // πŸ”₯ ULTRA FIX: Show success balloon like hoofdgrid if let gridFrame = self.getGridWindowFrame() { let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Removed!") panel.show(aroundGridFrame: gridFrame, text: "Item removed!", autoCloseAfter: 1.5) } } } catch { print("❌ Failed to remove stash item: \(error)") // πŸ”₯ ULTRA FIX: Show error balloon if let gridFrame = getGridWindowFrame() { let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Error") panel.show(aroundGridFrame: gridFrame, text: "Failed to remove item", autoCloseAfter: 2.0) } } } private func handleStashClose() { print("❌ StashGridActionDelegate: Independent close stash") // Find and close the stash window DispatchQueue.main.async { for window in NSApp.windows { if window.title.contains("Stash") { window.close() break } } } } } #if DEBUG struct IntegratedGalleryView_Previews: PreviewProvider { static var previews: some View { let store = GalleryImageStore() let sampleImage = NSImage(systemSymbolName: "photo", accessibilityDescription: nil) ?? NSImage() return IntegratedGalleryView(imageStore: store, initialImage: sampleImage, initialImageURL: nil, initialImageName: nil, hostingWindow: nil, closeAction: { print("Preview close action") }) .frame(width: 250, height: 300) } } #endif extension IntegratedGalleryView: Identifiable { var id: UUID { return galleryID } // _galleryID is al gedefinieerd in de struct. } // MARK: - OptimizedStashItemView struct OptimizedStashItemView: View { let imageItem: IdentifiableImage let calculatedThumbnailSize: CGFloat let thumbnailCornerRadius: CGFloat let hoverScaleEffect: CGFloat @Binding var hoveredImageID: UUID? @Binding var currentPreviewName: String // TURBO: Instant naam binding @Binding var imageForPreview: NSImage? @Binding var showPreview: Bool @Binding var isPreviewStable: Bool @Binding var hoverWorkItem: DispatchWorkItem? @Binding var stableDelegateCache: [UUID: StashDragDelegate] let imageStore: GalleryImageStore let onRemove: (IdentifiableImage) -> Void // CRITICAL FIX: Stable state to prevent re-renders @State private var tempURL: URL? @State private var isInitialized = false var body: some View { ZStack(alignment: .topTrailing) { // Main draggable image view if let url = tempURL { StashDraggableImageView( nsImage: imageItem.nsImage, imageURL: url, suggestedName: displayNameForStashItem(imageItem), stashItem: imageItem, delegate: getStableDelegate() ) .frame(width: calculatedThumbnailSize, height: calculatedThumbnailSize) .clipShape(RoundedRectangle(cornerRadius: thumbnailCornerRadius)) .scaleEffect(hoveredImageID == imageItem.id ? hoverScaleEffect : 1.0) .animation(.easeInOut(duration: 0.08), value: hoveredImageID == imageItem.id) .zIndex(hoveredImageID == imageItem.id ? 1 : 0) .allowsHitTesting(true) .contentShape(Rectangle()) .onTapGesture(count: 2) { openImageInSystemApp(imageItem, tempURL: url) } } else { // Fallback image Image(nsImage: imageItem.nsImage) .resizable() .scaledToFit() .cornerRadius(thumbnailCornerRadius) .frame(width: calculatedThumbnailSize, height: calculatedThumbnailSize) .clipped() .scaleEffect(hoveredImageID == imageItem.id ? hoverScaleEffect : 1.0) .animation(.easeInOut(duration: 0.08), value: hoveredImageID == imageItem.id) .zIndex(hoveredImageID == imageItem.id ? 1 : 0) .allowsHitTesting(true) .contentShape(Rectangle()) } // Remove button (X) Button(action: { onRemove(imageItem) }) { Image(systemName: "xmark.circle.fill") .foregroundColor(.black) .background(Color.white.opacity(0.6)) .clipShape(Circle()) .font(.system(size: 13)) } .buttonStyle(.plain) .offset(x: 3, y: -3) .opacity(hoveredImageID == imageItem.id ? 1 : 0) .animation(.easeInOut(duration: 0.08), value: hoveredImageID == imageItem.id) .zIndex(2) .allowsHitTesting(hoveredImageID == imageItem.id) } .frame(width: calculatedThumbnailSize, height: calculatedThumbnailSize) .background(Color.clear) .contentShape(Rectangle()) .onHover { isHovering in // OPTIMIZED: Minimal state changes hoverWorkItem?.cancel() if isHovering { self.hoveredImageID = imageItem.id // πŸš€ TURBO INSTANT UPDATE: Zet naam METEEN zonder delay! self.currentPreviewName = imageItem.fileURL?.deletingPathExtension().lastPathComponent ?? "Unknown" print("πŸ” ULTRA DEBUG: onHover START - set hoveredImageID to \(imageItem.id) with INSTANT name: '\(self.currentPreviewName)' from item: \(imageItem.fileURL?.lastPathComponent ?? "nil")") self.imageForPreview = imageItem.nsImage self.showPreview = true self.isPreviewStable = true } else { if self.hoveredImageID == imageItem.id { print("πŸ” ULTRA DEBUG: onHover END - clearing hover for \(imageItem.id)") let workItem = DispatchWorkItem { if self.hoveredImageID == imageItem.id { self.hoveredImageID = nil self.currentPreviewName = "Unknown" // Reset naam ook self.imageForPreview = nil self.showPreview = false self.isPreviewStable = false } } self.hoverWorkItem = workItem DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: workItem) } } } .onAppear { // CRITICAL FIX: Initialize only once if !isInitialized { initializeTempURL() isInitialized = true } } } // CRITICAL FIX: Stable delegate getter that prevents recreation private func getStableDelegate() -> StashDragDelegate { if let existingDelegate = stableDelegateCache[imageItem.id] { return existingDelegate } else { let newDelegate = StashDragDelegate(imageItem: imageItem, imageStore: imageStore) stableDelegateCache[imageItem.id] = newDelegate return newDelegate } } private func initializeTempURL() { if let existingURL = imageItem.fileURL { tempURL = existingURL } else { tempURL = createTempURLForStashItem(imageItem) } } private func createTempURLForStashItem(_ imgItem: IdentifiableImage) -> URL? { if let existingURL = imgItem.fileURL { return existingURL } guard let tiffRepresentation = imgItem.nsImage.tiffRepresentation, let bitmapImageRep = NSBitmapImageRep(data: tiffRepresentation), let pngData = bitmapImageRep.representation(using: .png, properties: [:]) else { return nil } let tempDirectoryURL = FileManager.default.temporaryDirectory let tempFilename = "\(imgItem.id.uuidString).png" let tempFileURL = tempDirectoryURL.appendingPathComponent(tempFilename) do { try pngData.write(to: tempFileURL) return tempFileURL } catch { return nil } } private func openImageInSystemApp(_ imgItem: IdentifiableImage, tempURL: URL) { NSWorkspace.shared.open(tempURL) } private func displayNameForStashItem(_ imgItem: IdentifiableImage) -> String? { if let customName = imgItem.customName, !customName.isEmpty { return customName } if let fileURL = imgItem.fileURL { return fileURL.deletingPathExtension().lastPathComponent } return nil } }