diff --git a/AppIcon.icns b/AppIcon.icns deleted file mode 100644 index de21e75..0000000 Binary files a/AppIcon.icns and /dev/null differ diff --git a/Info.plist b/Info.plist deleted file mode 100644 index fd861e5..0000000 --- a/Info.plist +++ /dev/null @@ -1,67 +0,0 @@ - - - - - CFBundleDisplayName - ShotScreen - CFBundleExecutable - ShotScreen - CFBundleGetInfoString - ShotScreen - Professional Screenshot Tool - CFBundleIconFile - AppIcon - CFBundleIdentifier - com.shotscreen.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - ShotScreen - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1.0 - LSApplicationCategoryType - public.app-category.utilities - LSMinimumSystemVersion - 13.0 - LSUIElement - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - NSExceptionMinimumTLSVersion - TLSv1.3 - NSRequiresCertificateTransparency - - - NSApplicationDescription - Professional screenshot and screen capture utility for macOS - NSCameraUsageDescription - ShotScreen needs camera access to capture screenshots. - NSDesktopFolderUsageDescription - ShotScreen needs access to save screenshots to your Desktop. - NSDocumentsFolderUsageDescription - ShotScreen needs access to save screenshots to your Documents folder. - NSHighResolutionCapable - - NSHumanReadableCopyright - ยฉ 2025 ShotScreen. All rights reserved. - NSScreenCaptureUsageDescription - ShotScreen is a professional screenshot tool that captures images of your screen and windows to help you save and organize screenshots efficiently. - NSSupportsAutomaticGraphicsSwitching - - SUAutomaticallyUpdate - - SUCheckAtStartup - - SUFeedURL - https://git.plet.i234.me/Nick/shotscreen/raw/branch/main/appcast.xml - SUPublicEDKey - q0Ia/obtuDugqhwa1aSZsQAqZxQsdgX3y4K9wuqkemM= - SUScheduledCheckInterval - 86400 - - diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 6a79eeb..0000000 --- a/Package.resolved +++ /dev/null @@ -1,23 +0,0 @@ -{ - "pins" : [ - { - "identity" : "hotkey", - "kind" : "remoteSourceControl", - "location" : "https://github.com/soffes/HotKey", - "state" : { - "revision" : "a3cf605d7a96f6ff50e04fcb6dea6e2613cfcbe4", - "version" : "0.2.1" - } - }, - { - "identity" : "sparkle", - "kind" : "remoteSourceControl", - "location" : "https://github.com/sparkle-project/Sparkle", - "state" : { - "revision" : "df074165274afaa39539c05d57b0832620775b11", - "version" : "2.7.1" - } - } - ], - "version" : 2 -} diff --git a/Package.swift b/Package.swift deleted file mode 100755 index 03b7be3..0000000 --- a/Package.swift +++ /dev/null @@ -1,59 +0,0 @@ -// swift-tools-version:5.9 -import PackageDescription - -let package = Package( - name: "ShotScreen", - platforms: [ - .macOS(.v13) - ], - dependencies: [ - .package(url: "https://github.com/soffes/HotKey", from: "0.1.4"), - .package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0") - ], - targets: [ - .executableTarget( - name: "ShotScreen", - dependencies: [ - .product(name: "HotKey", package: "HotKey"), - .product(name: "Sparkle", package: "Sparkle") - ], - path: "ShotScreen/Sources", - sources: [ - "BackgroundRemover.swift", - "ButtonHoverExtension.swift", - "Config.swift", - "CrosshairViews.swift", - "DesktopIconManager.swift", - "DraggableImageView.swift", - "EventCapture.swift", - "FeedbackBubblePanel.swift", - "FinderWindowManager.swift", - "FirstLaunchWizard.swift", - "GridActionManager.swift", - "GridCellView.swift", - "GridComponents.swift", - "IntegratedGalleryView.swift", - "LicenseEntryView.swift", - "LicenseManager.swift", - "MenuManager.swift", - "MultiMonitorSystem.swift", - "OverlayComponents.swift", - "PreviewManager.swift", - "RenameActionHandler.swift", - "ScreenCaptureKitProvider.swift", - "SettingsManager.swift", - "SettingsModels.swift", - "SettingsUI.swift", - "StashDraggableImageView.swift", - "SwiftUIViews.swift", - "ThemeManager.swift", - "UpdateManager.swift", - "WindowCaptureManager.swift", - "main.swift" - ], - resources: [ - .process("images") - ] - ) - ] -) \ No newline at end of file diff --git a/Pixelmator images/BalloonS.pxd b/Pixelmator images/BalloonS.pxd deleted file mode 100644 index 2f0e744..0000000 Binary files a/Pixelmator images/BalloonS.pxd and /dev/null differ diff --git a/Pixelmator images/Banner.pxd b/Pixelmator images/Banner.pxd deleted file mode 100644 index 36b976d..0000000 Binary files a/Pixelmator images/Banner.pxd and /dev/null differ diff --git a/Pixelmator images/Logo.pxd b/Pixelmator images/Logo.pxd deleted file mode 100644 index e012f45..0000000 Binary files a/Pixelmator images/Logo.pxd and /dev/null differ diff --git a/Pixelmator images/Logo_200x200px.pxd b/Pixelmator images/Logo_200x200px.pxd deleted file mode 100644 index 1914948..0000000 Binary files a/Pixelmator images/Logo_200x200px.pxd and /dev/null differ diff --git a/Pixelmator images/MenuBarIcon.pxd b/Pixelmator images/MenuBarIcon.pxd deleted file mode 100644 index c3b3d18..0000000 Binary files a/Pixelmator images/MenuBarIcon.pxd and /dev/null differ diff --git a/Pixelmator images/Thumbnail_Met_Achtergrond.pxd b/Pixelmator images/Thumbnail_Met_Achtergrond.pxd deleted file mode 100644 index 817a3e7..0000000 Binary files a/Pixelmator images/Thumbnail_Met_Achtergrond.pxd and /dev/null differ diff --git a/ShotScreen/.DS_Store b/ShotScreen/.DS_Store deleted file mode 100644 index b4db831..0000000 Binary files a/ShotScreen/.DS_Store and /dev/null differ diff --git a/ShotScreen/.build/build.db b/ShotScreen/.build/build.db deleted file mode 100755 index bd394fb..0000000 Binary files a/ShotScreen/.build/build.db and /dev/null differ diff --git a/ShotScreen/.build/debug b/ShotScreen/.build/debug deleted file mode 100755 index ef80fcc..0000000 --- a/ShotScreen/.build/debug +++ /dev/null @@ -1,5 +0,0 @@ -XSym -0024 -65d970057a31dc065e5b25921548a548 -arm64-apple-macosx/debug - \ No newline at end of file diff --git a/ShotScreen/.build/debug.yaml b/ShotScreen/.build/debug.yaml deleted file mode 100755 index dc4bd49..0000000 --- a/ShotScreen/.build/debug.yaml +++ /dev/null @@ -1,100 +0,0 @@ -client: - name: basic - file-system: device-agnostic -tools: {} -targets: - "HotKey-arm64-apple-macosx15.0-debug.module": [""] - "PackageStructure": [""] - "ScreenShot-arm64-apple-macosx15.0-debug.exe": [""] - "ScreenShot-arm64-apple-macosx15.0-debug.module": [""] - "main": ["",""] - "test": ["",""] -default: "main" -nodes: - "/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/": - is-directory-structure: true - content-exclusion-patterns: [".git",".build"] - "/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot": - is-mutated: true -commands: - "/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/sources": - tool: write-auxiliary-file - inputs: ["","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/HotKey.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/HotKeysController.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/Key.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/KeyCombo+System.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/KeyCombo.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/NSEventModifierFlags+HotKey.swift"] - outputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/sources"] - description: "Write auxiliary file /Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/sources" - - "/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot-entitlement.plist": - tool: write-auxiliary-file - inputs: ["",""] - outputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot-entitlement.plist"] - description: "Write auxiliary file /Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot-entitlement.plist" - - "/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/sources": - tool: write-auxiliary-file - inputs: ["","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/GridViewManager.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/RenameActionHandler.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/Settings.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/main.swift"] - outputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/sources"] - description: "Write auxiliary file /Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/sources" - - "/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.product/Objects.LinkFileList": - tool: write-auxiliary-file - inputs: ["","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKey.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKeysController.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/Key.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/KeyCombo+System.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/KeyCombo.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/NSEventModifierFlags+HotKey.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/GridViewManager.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/RenameActionHandler.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/Settings.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/main.swift.o"] - outputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.product/Objects.LinkFileList"] - description: "Write auxiliary file /Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.product/Objects.LinkFileList" - - "/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/swift-version--2F68581710C0F3BB.txt": - tool: write-auxiliary-file - inputs: ["","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc"] - outputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/swift-version--2F68581710C0F3BB.txt"] - always-out-of-date: "true" - description: "Write auxiliary file /Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/swift-version--2F68581710C0F3BB.txt" - - "": - tool: phony - inputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKey.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKeysController.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/Key.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/KeyCombo+System.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/KeyCombo.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/NSEventModifierFlags+HotKey.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/HotKey.swiftmodule"] - outputs: [""] - - "": - tool: phony - inputs: [""] - outputs: [""] - - "": - tool: phony - inputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/GridViewManager.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/RenameActionHandler.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/Settings.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/main.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/ScreenShot.swiftmodule"] - outputs: [""] - - "C.HotKey-arm64-apple-macosx15.0-debug.module": - tool: shell - inputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/HotKey.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/HotKeysController.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/Key.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/KeyCombo+System.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/KeyCombo.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/NSEventModifierFlags+HotKey.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/swift-version--2F68581710C0F3BB.txt","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/sources"] - outputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKey.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKeysController.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/Key.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/KeyCombo+System.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/KeyCombo.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/NSEventModifierFlags+HotKey.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/HotKey.swiftmodule"] - description: "Compiling Swift Module 'HotKey' (6 sources)" - args: ["/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc","-module-name","HotKey","-emit-dependencies","-emit-module","-emit-module-path","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/HotKey.swiftmodule","-output-file-map","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/output-file-map.json","-parse-as-library","-incremental","-c","@/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/sources","-I","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules","-target","arm64-apple-macosx10.13","-enable-batch-mode","-index-store-path","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/index/store","-Onone","-enable-testing","-j10","-DSWIFT_PACKAGE","-DDEBUG","-module-cache-path","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ModuleCache","-parseable-output","-parse-as-library","-emit-objc-header","-emit-objc-header-path","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKey-Swift.h","-color-diagnostics","-swift-version","5","-sdk","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk","-F","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks","-F","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/PrivateFrameworks","-I","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib","-L","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib","-g","-Xcc","-isysroot","-Xcc","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk","-Xcc","-F","-Xcc","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks","-Xcc","-F","-Xcc","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/PrivateFrameworks","-Xcc","-fPIC","-Xcc","-g","-suppress-warnings"] - - "C.ScreenShot-arm64-apple-macosx15.0-debug.exe": - tool: shell - inputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKey.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKeysController.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/Key.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/KeyCombo+System.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/KeyCombo.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/NSEventModifierFlags+HotKey.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/GridViewManager.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/RenameActionHandler.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/Settings.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/main.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.product/Objects.LinkFileList"] - outputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot"] - description: "Linking ./.build/arm64-apple-macosx/debug/ScreenShot" - args: ["/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc","-L","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug","-o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot","-module-name","ScreenShot","-Xlinker","-no_warn_duplicate_libraries","-emit-executable","-Xlinker","-alias","-Xlinker","_ScreenShot_main","-Xlinker","_main","-Xlinker","-rpath","-Xlinker","@loader_path","@/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.product/Objects.LinkFileList","-target","arm64-apple-macosx13.0","-Xlinker","-add_ast_path","-Xlinker","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/HotKey.swiftmodule","-Xlinker","-add_ast_path","-Xlinker","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/ScreenShot.swiftmodule","-sdk","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk","-F","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks","-F","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/PrivateFrameworks","-I","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib","-L","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib","-g"] - - "C.ScreenShot-arm64-apple-macosx15.0-debug.exe-entitlements": - tool: shell - inputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot-entitlement.plist"] - outputs: [""] - description: "Applying debug entitlements to ./.build/arm64-apple-macosx/debug/ScreenShot" - args: ["codesign","--force","--sign","-","--entitlements","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot-entitlement.plist","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot"] - - "C.ScreenShot-arm64-apple-macosx15.0-debug.module": - tool: shell - inputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/GridViewManager.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/RenameActionHandler.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/Settings.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/main.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/swift-version--2F68581710C0F3BB.txt","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/HotKey.swiftmodule","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/sources"] - outputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/GridViewManager.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/RenameActionHandler.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/Settings.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/main.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/ScreenShot.swiftmodule"] - description: "Compiling Swift Module 'ScreenShot' (4 sources)" - args: ["/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc","-module-name","ScreenShot","-emit-dependencies","-emit-module","-emit-module-path","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/ScreenShot.swiftmodule","-output-file-map","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/output-file-map.json","-incremental","-c","@/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/sources","-I","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules","-target","arm64-apple-macosx13.0","-enable-batch-mode","-index-store-path","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/index/store","-Onone","-enable-testing","-j10","-DSWIFT_PACKAGE","-DDEBUG","-module-cache-path","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ModuleCache","-parseable-output","-Xfrontend","-entry-point-function-name","-Xfrontend","ScreenShot_main","-color-diagnostics","-swift-version","5","-sdk","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk","-F","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks","-F","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/PrivateFrameworks","-I","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib","-L","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib","-g","-Xcc","-isysroot","-Xcc","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk","-Xcc","-F","-Xcc","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks","-Xcc","-F","-Xcc","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/PrivateFrameworks","-Xcc","-fPIC","-Xcc","-g","-package-name","screenshot"] - - "PackageStructure": - tool: package-structure-tool - inputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Package.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Package.resolved"] - outputs: [""] - description: "Planning build" - allow-missing-inputs: true - diff --git a/ShotScreen/.build/plugin-tools.yaml b/ShotScreen/.build/plugin-tools.yaml deleted file mode 100755 index dc4bd49..0000000 --- a/ShotScreen/.build/plugin-tools.yaml +++ /dev/null @@ -1,100 +0,0 @@ -client: - name: basic - file-system: device-agnostic -tools: {} -targets: - "HotKey-arm64-apple-macosx15.0-debug.module": [""] - "PackageStructure": [""] - "ScreenShot-arm64-apple-macosx15.0-debug.exe": [""] - "ScreenShot-arm64-apple-macosx15.0-debug.module": [""] - "main": ["",""] - "test": ["",""] -default: "main" -nodes: - "/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/": - is-directory-structure: true - content-exclusion-patterns: [".git",".build"] - "/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot": - is-mutated: true -commands: - "/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/sources": - tool: write-auxiliary-file - inputs: ["","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/HotKey.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/HotKeysController.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/Key.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/KeyCombo+System.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/KeyCombo.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/NSEventModifierFlags+HotKey.swift"] - outputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/sources"] - description: "Write auxiliary file /Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/sources" - - "/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot-entitlement.plist": - tool: write-auxiliary-file - inputs: ["",""] - outputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot-entitlement.plist"] - description: "Write auxiliary file /Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot-entitlement.plist" - - "/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/sources": - tool: write-auxiliary-file - inputs: ["","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/GridViewManager.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/RenameActionHandler.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/Settings.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/main.swift"] - outputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/sources"] - description: "Write auxiliary file /Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/sources" - - "/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.product/Objects.LinkFileList": - tool: write-auxiliary-file - inputs: ["","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKey.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKeysController.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/Key.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/KeyCombo+System.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/KeyCombo.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/NSEventModifierFlags+HotKey.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/GridViewManager.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/RenameActionHandler.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/Settings.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/main.swift.o"] - outputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.product/Objects.LinkFileList"] - description: "Write auxiliary file /Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.product/Objects.LinkFileList" - - "/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/swift-version--2F68581710C0F3BB.txt": - tool: write-auxiliary-file - inputs: ["","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc"] - outputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/swift-version--2F68581710C0F3BB.txt"] - always-out-of-date: "true" - description: "Write auxiliary file /Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/swift-version--2F68581710C0F3BB.txt" - - "": - tool: phony - inputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKey.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKeysController.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/Key.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/KeyCombo+System.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/KeyCombo.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/NSEventModifierFlags+HotKey.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/HotKey.swiftmodule"] - outputs: [""] - - "": - tool: phony - inputs: [""] - outputs: [""] - - "": - tool: phony - inputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/GridViewManager.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/RenameActionHandler.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/Settings.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/main.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/ScreenShot.swiftmodule"] - outputs: [""] - - "C.HotKey-arm64-apple-macosx15.0-debug.module": - tool: shell - inputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/HotKey.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/HotKeysController.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/Key.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/KeyCombo+System.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/KeyCombo.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/checkouts/HotKey/Sources/HotKey/NSEventModifierFlags+HotKey.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/swift-version--2F68581710C0F3BB.txt","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/sources"] - outputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKey.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKeysController.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/Key.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/KeyCombo+System.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/KeyCombo.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/NSEventModifierFlags+HotKey.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/HotKey.swiftmodule"] - description: "Compiling Swift Module 'HotKey' (6 sources)" - args: ["/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc","-module-name","HotKey","-emit-dependencies","-emit-module","-emit-module-path","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/HotKey.swiftmodule","-output-file-map","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/output-file-map.json","-parse-as-library","-incremental","-c","@/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/sources","-I","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules","-target","arm64-apple-macosx10.13","-enable-batch-mode","-index-store-path","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/index/store","-Onone","-enable-testing","-j10","-DSWIFT_PACKAGE","-DDEBUG","-module-cache-path","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ModuleCache","-parseable-output","-parse-as-library","-emit-objc-header","-emit-objc-header-path","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKey-Swift.h","-color-diagnostics","-swift-version","5","-sdk","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk","-F","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks","-F","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/PrivateFrameworks","-I","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib","-L","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib","-g","-Xcc","-isysroot","-Xcc","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk","-Xcc","-F","-Xcc","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks","-Xcc","-F","-Xcc","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/PrivateFrameworks","-Xcc","-fPIC","-Xcc","-g","-suppress-warnings"] - - "C.ScreenShot-arm64-apple-macosx15.0-debug.exe": - tool: shell - inputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKey.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/HotKeysController.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/Key.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/KeyCombo+System.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/KeyCombo.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/HotKey.build/NSEventModifierFlags+HotKey.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/GridViewManager.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/RenameActionHandler.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/Settings.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/main.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.product/Objects.LinkFileList"] - outputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot"] - description: "Linking ./.build/arm64-apple-macosx/debug/ScreenShot" - args: ["/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc","-L","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug","-o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot","-module-name","ScreenShot","-Xlinker","-no_warn_duplicate_libraries","-emit-executable","-Xlinker","-alias","-Xlinker","_ScreenShot_main","-Xlinker","_main","-Xlinker","-rpath","-Xlinker","@loader_path","@/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.product/Objects.LinkFileList","-target","arm64-apple-macosx13.0","-Xlinker","-add_ast_path","-Xlinker","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/HotKey.swiftmodule","-Xlinker","-add_ast_path","-Xlinker","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/ScreenShot.swiftmodule","-sdk","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk","-F","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks","-F","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/PrivateFrameworks","-I","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib","-L","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib","-g"] - - "C.ScreenShot-arm64-apple-macosx15.0-debug.exe-entitlements": - tool: shell - inputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot-entitlement.plist"] - outputs: [""] - description: "Applying debug entitlements to ./.build/arm64-apple-macosx/debug/ScreenShot" - args: ["codesign","--force","--sign","-","--entitlements","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot-entitlement.plist","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot"] - - "C.ScreenShot-arm64-apple-macosx15.0-debug.module": - tool: shell - inputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/GridViewManager.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/RenameActionHandler.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/Settings.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/main.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/swift-version--2F68581710C0F3BB.txt","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/HotKey.swiftmodule","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/sources"] - outputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/GridViewManager.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/RenameActionHandler.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/Settings.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/main.swift.o","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/ScreenShot.swiftmodule"] - description: "Compiling Swift Module 'ScreenShot' (4 sources)" - args: ["/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swiftc","-module-name","ScreenShot","-emit-dependencies","-emit-module","-emit-module-path","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules/ScreenShot.swiftmodule","-output-file-map","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/output-file-map.json","-incremental","-c","@/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ScreenShot.build/sources","-I","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/Modules","-target","arm64-apple-macosx13.0","-enable-batch-mode","-index-store-path","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/index/store","-Onone","-enable-testing","-j10","-DSWIFT_PACKAGE","-DDEBUG","-module-cache-path","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/.build/arm64-apple-macosx/debug/ModuleCache","-parseable-output","-Xfrontend","-entry-point-function-name","-Xfrontend","ScreenShot_main","-color-diagnostics","-swift-version","5","-sdk","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk","-F","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks","-F","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/PrivateFrameworks","-I","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib","-L","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib","-g","-Xcc","-isysroot","-Xcc","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk","-Xcc","-F","-Xcc","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks","-Xcc","-F","-Xcc","/Volumes/External HD/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/PrivateFrameworks","-Xcc","-fPIC","-Xcc","-g","-package-name","screenshot"] - - "PackageStructure": - tool: package-structure-tool - inputs: ["/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Sources/","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Package.swift","/Volumes/homes/Nick Roodenrijs/ScreenShot 7/ScreenShot/Package.resolved"] - outputs: [""] - description: "Planning build" - allow-missing-inputs: true - diff --git a/ShotScreen/Sources/BackgroundRemover.swift b/ShotScreen/Sources/BackgroundRemover.swift deleted file mode 100644 index 03e1602..0000000 --- a/ShotScreen/Sources/BackgroundRemover.swift +++ /dev/null @@ -1,1615 +0,0 @@ -import Foundation -import AppKit -import Vision -import CoreImage -import CoreImage.CIFilterBuiltins -import CoreML -import SwiftUI -import Darwin // For utsname system info - -// MARK: - Background Removal Manager -class BackgroundRemover { - static let shared = BackgroundRemover() - - enum ProcessingMethod { - case visionFramework - case coreMLRMBG14 - } - - private init() {} - - // MARK: - Public Interface - func removeBackground(from image: NSImage, method: ProcessingMethod, completion: @escaping (NSImage?) -> Void) { - guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { - print("โŒ BackgroundRemover: Could not convert NSImage to CGImage") - completion(nil) - return - } - - DispatchQueue.global(qos: .userInitiated).async { - switch method { - case .visionFramework: - self.processWithVisionFramework(cgImage: cgImage, completion: completion) - case .coreMLRMBG14: - self.processWithCoreMLModelWithFallback(cgImage: cgImage, completion: completion) - } - } - } - - // ๐ŸŽฏ NEW: Process with user's preferred method from settings (with smart fallback) - func processWithPreferredMethod(from image: NSImage, completion: @escaping (NSImage?) -> Void) { - let preferredMethod = SettingsManager.shared.preferredBackgroundRemovalMethod - - switch preferredMethod { - case .auto: - // Auto mode: try RMBG first, fallback to Vision - processWithCoreMLModelWithFallback(from: image, completion: completion) - case .rmbg: - // RMBG only (no fallback) - processWithCoreMLRMBGOnly(from: image, completion: completion) - case .vision: - // Vision Framework only - processWithVisionFramework(from: image, completion: completion) - } - } - - // ๐ŸŽฏ NEW: Process with CoreML model and fallback to Vision - private func processWithCoreMLModelWithFallback(from image: NSImage, completion: @escaping (NSImage?) -> Void) { - guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { - print("โŒ BackgroundRemover: Could not convert NSImage to CGImage") - completion(nil) - return - } - - DispatchQueue.global(qos: .userInitiated).async { - self.processWithCoreMLModelWithFallback(cgImage: cgImage, completion: completion) - } - } - - // ๐ŸŽฏ NEW: Process with Vision Framework only - private func processWithVisionFramework(from image: NSImage, completion: @escaping (NSImage?) -> Void) { - guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { - print("โŒ BackgroundRemover: Could not convert NSImage to CGImage") - completion(nil) - return - } - - DispatchQueue.global(qos: .userInitiated).async { - self.processWithVisionFramework(cgImage: cgImage, completion: completion) - } - } - - // ๐ŸŽฏ NEW: Process with RMBG only (no fallback) - private func processWithCoreMLRMBGOnly(from image: NSImage, completion: @escaping (NSImage?) -> Void) { - guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { - print("โŒ BackgroundRemover: Could not convert NSImage to CGImage") - completion(nil) - return - } - - DispatchQueue.global(qos: .userInitiated).async { - self.processWithCoreMLRMBGOnly(cgImage: cgImage, completion: completion) - } - } - - // MARK: - Smart E5RT Issue Detection - private func shouldSkipRMBGDueToIssues() -> Bool { - // Check if we have persistent E5RT cache issues that suggest we should avoid RMBG - let issueThreshold = 3 // Skip after 3 attempts with issues - let recentFailures = UserDefaults.standard.integer(forKey: "ShotScreen_E5RT_FailureCount") - - if recentFailures >= issueThreshold { - let lastSkip = UserDefaults.standard.double(forKey: "ShotScreen_E5RT_LastSkip") - let timeSinceSkip = Date().timeIntervalSince1970 - lastSkip - - // Reset after 1 hour, in case the issue was temporary - if timeSinceSkip > 3600 { - UserDefaults.standard.set(0, forKey: "ShotScreen_E5RT_FailureCount") - UserDefaults.standard.removeObject(forKey: "ShotScreen_E5RT_LastSkip") - return false - } - - return true - } - - return false - } - - private func recordE5RTIssue() { - let currentCount = UserDefaults.standard.integer(forKey: "ShotScreen_E5RT_FailureCount") - UserDefaults.standard.set(currentCount + 1, forKey: "ShotScreen_E5RT_FailureCount") - UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "ShotScreen_E5RT_LastSkip") - print("๐Ÿ”ง E5RT Issue recorded: Total count = \(currentCount + 1)") - - // If we're getting too many E5RT issues, disable Neural Engine more aggressively - if currentCount >= 3 { - print("๐Ÿ”ง TOO MANY E5RT ISSUES: Disabling Neural Engine for 5 minutes") - UserDefaults.standard.set(true, forKey: "ShotScreen_ForceCPU_TempMode") - UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "ShotScreen_ForceCPU_StartTime_Extended") - } - } - - // ๐ŸŽฏ NEW: RMBG-only processing (no fallback) - private func processWithCoreMLRMBGOnly(cgImage: CGImage, completion: @escaping (NSImage?) -> Void) { - if tryLoadAndProcessModel(cgImage: cgImage, modelName: "bria-rmbg-coreml", displayName: "RMBG-1.4", completion: completion) { - // RMBG-1.4 processing initiated successfully - return - } - - // Failed to load RMBG model - print("โŒ RMBG-1.4 failed and no fallback allowed by user setting") - DispatchQueue.main.async { - completion(nil) - } - } - - - - // MARK: - Model Availability - private func getModelPath() -> String { - let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! - let appDirectory = appSupportURL.appendingPathComponent("ShotScreen") - return appDirectory.appendingPathComponent("bria-rmbg-coreml.mlpackage").path - } - - func isRMBGModelAvailable() -> Bool { - return FileManager.default.fileExists(atPath: getModelPath()) - } - - // MARK: - Public Cache Management - func clearCoreMLCachePublic() { - print("๐Ÿงน PUBLIC: User requested Core ML cache clearing...") - - // Do this on background thread to avoid blocking UI - DispatchQueue.global(qos: .utility).async { - self.clearCoreMLCache() - - // Reset failure count since user manually cleared cache - UserDefaults.standard.set(0, forKey: "ShotScreen_E5RT_FailureCount") - UserDefaults.standard.removeObject(forKey: "ShotScreen_E5RT_LastSkip") - - DispatchQueue.main.async { - print("โœ… Cache clearing completed successfully!") - } - } - } - - - - // MARK: - Cache Management (ULTRA AGGRESSIVE E5RT fix) - private func clearCoreMLCache() { - print("๐Ÿงน ULTRA AGGRESSIVE: Nuking ALL Core ML E5RT caches...") - - // STRATEGY 1: Direct file removal (fastest) - ultraFastCacheClear() - - // STRATEGY 2: Force CPU-only mode temporarily - temporarilyForceCPUMode() - - // STRATEGY 3: Model precompilation with fresh cache - forceModelRecompilation() - - print("๐Ÿงน ULTRA AGGRESSIVE: E5RT cache nuking completed") - } - - private func ultraFastCacheClear() { - // Use rm -rf for maximum speed (faster than FileManager) - let cachePaths = [ - "~/Library/Caches/com.apple.e5rt.e5bundlecache", - "~/Library/Caches/com.apple.CoreML", - "~/Library/Caches/com.apple.mlcompute", - "~/Library/Caches/ShotScreen" - ] - - for path in cachePaths { - let expandedPath = NSString(string: path).expandingTildeInPath - let command = "rm -rf '\(expandedPath)' 2>/dev/null" - - let process = Process() - process.launchPath = "/bin/sh" - process.arguments = ["-c", command] - - do { - try process.run() - process.waitUntilExit() - print("โœ… NUKED: \(path)") - } catch { - print("โš ๏ธ Could not nuke: \(path)") - } - } - } - - private func temporarilyForceCPUMode() { - // Set flag to force CPU mode for next few loads - UserDefaults.standard.set(true, forKey: "ShotScreen_ForceCPU_TempMode") - UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "ShotScreen_ForceCPU_StartTime") - print("๐Ÿ”ง TEMPORARY: Forcing CPU-only mode for next 60 seconds") - } - - private func forceModelRecompilation() { - // Delete any existing compiled model to force fresh compilation - let modelPath = getModelPath() - let modelURL = URL(fileURLWithPath: modelPath) - - // Remove compiled version if it exists - do { - let compiledURL = try MLModel.compileModel(at: modelURL) - if FileManager.default.fileExists(atPath: compiledURL.path) { - try FileManager.default.removeItem(at: compiledURL) - print("โœ… FORCED: Model recompilation") - } - } catch { - // Ignore errors - we just want to force recompilation - } - } - - private func clearWildcardCachePath(_ pattern: String) { - // Handle /var/folders/*/com.apple.e5rt.e5bundlecache patterns - let components = pattern.components(separatedBy: "*") - guard components.count == 2 else { return } - - let prefix = components[0] - let suffix = components[1] - - do { - let prefixURL = URL(fileURLWithPath: prefix) - let contents = try FileManager.default.contentsOfDirectory( - at: prefixURL, - includingPropertiesForKeys: nil - ) - - for item in contents { - let targetPath = item.appendingPathComponent(String(suffix.dropFirst())) - if FileManager.default.fileExists(atPath: targetPath.path) { - try FileManager.default.removeItem(at: targetPath) - print("โœ… Cleared system cache: \(targetPath.path)") - } - } - } catch { - print("โš ๏ธ Could not clear wildcard cache \(pattern): \(error.localizedDescription)") - } - } - - private func clearCompiledModelCache() { - // Try to find and clear any compiled versions of our model - let modelPath = getModelPath() - let modelURL = URL(fileURLWithPath: modelPath) - - do { - // Force recompilation by removing any existing compiled versions - let compiledURL = try MLModel.compileModel(at: modelURL) - if FileManager.default.fileExists(atPath: compiledURL.path) { - try FileManager.default.removeItem(at: compiledURL) - print("โœ… Cleared compiled model cache") - } - } catch { - print("โš ๏ธ Could not clear compiled model cache: \(error.localizedDescription)") - } - } - - // MARK: - Optimized Model Loading with Proactive Cache Management - private func loadModelWithRetry(at url: URL, retryCount: Int = 0) throws -> MLModel { - // Proactively clear cache on first attempt if we've seen issues before - if retryCount == 0 && hasRecentCacheIssues() { - print("๐Ÿงน Proactively clearing E5RT cache due to recent issues...") - clearCoreMLCache() - Thread.sleep(forTimeInterval: 0.5) - } - - do { - let config = MLModelConfiguration() - config.computeUnits = getOptimalComputeUnits() - - // Add cache preferences to avoid E5RT issues - if #available(macOS 14.0, *) { - config.allowLowPrecisionAccumulationOnGPU = true - } - - return try MLModel(contentsOf: url, configuration: config) - } catch let error where retryCount < 2 && (error.localizedDescription.contains("resources.bin") || error.localizedDescription.contains("E5RT")) { - print("๐Ÿ”„ E5RT cache error detected, clearing cache and retrying... (attempt \(retryCount + 1)/3)") - clearCoreMLCache() - - // Wait longer for cache to properly clear - Thread.sleep(forTimeInterval: 1.5) - - return try loadModelWithRetry(at: url, retryCount: retryCount + 1) - } - } - - // MARK: - Cache Issue Detection - private func hasRecentCacheIssues() -> Bool { - // Check if we've seen cache issues recently by looking for E5RT cache directories - let problemIndicators = [ - "~/Library/Caches/ShotScreen/com.apple.e5rt.e5bundlecache", - "~/Library/Caches/com.apple.e5rt.e5bundlecache" - ] - - for location in problemIndicators { - let expandedPath = NSString(string: location).expandingTildeInPath - if FileManager.default.fileExists(atPath: expandedPath) { - // Check if directory is non-empty (has cached data that might be corrupt) - do { - let contents = try FileManager.default.contentsOfDirectory(atPath: expandedPath) - if !contents.isEmpty { - print("๐Ÿ” Found existing E5RT cache, potential for issues: \(location)") - return true - } - } catch { - // If we can't read it, it might be corrupt - return true - } - } - } - return false - } - - // MARK: - CPU-only Model Loading - private func loadModelWithCPUOnly(at url: URL, retryCount: Int = 0) throws -> MLModel { - do { - let config = MLModelConfiguration() - config.computeUnits = .cpuOnly - - return try MLModel(contentsOf: url, configuration: config) - } catch let error where retryCount < 2 && error.localizedDescription.contains("resources.bin") { - print("๐Ÿ”„ E5RT cache error in CPU mode, clearing cache and retrying... (attempt \(retryCount + 1)/3)") - clearCoreMLCache() - - // Wait a moment for cache to clear - Thread.sleep(forTimeInterval: 1.0) - - return try loadModelWithCPUOnly(at: url, retryCount: retryCount + 1) - } - } - - // MARK: - Architecture Detection & Compute Unit Optimization - private func getOptimalComputeUnits() -> MLComputeUnits { - // Check if we're in temporary CPU-only mode to avoid E5RT issues - if isInTemporaryCPUMode() { - print("๐Ÿ”ง TEMPORARY CPU MODE: Using CPU-only to avoid E5RT issues") - return .cpuOnly - } - - // Detect CPU architecture for optimal Core ML performance - var systemInfo = utsname() - uname(&systemInfo) - let machine = withUnsafePointer(to: &systemInfo.machine) { - $0.withMemoryRebound(to: CChar.self, capacity: 1) { - String(validatingUTF8: $0) - } - } - - let machineString = machine ?? "unknown" - print("๐Ÿ–ฅ๏ธ Detected machine architecture: \(machineString)") - - // Check for Apple Silicon (M1, M2, M3, etc.) - if machineString.contains("arm64") || machineString.hasPrefix("arm") { - print("๐Ÿš€ Apple Silicon detected - using all compute units (Neural Engine + GPU + CPU)") - return .all - } else { - // Intel Mac - use CPU and GPU only (no Neural Engine available) - print("โšก Intel Mac detected - using CPU and GPU only (no Neural Engine)") - return .cpuAndGPU - } - } - - private func isInTemporaryCPUMode() -> Bool { - guard UserDefaults.standard.bool(forKey: "ShotScreen_ForceCPU_TempMode") else { - return false - } - - let startTime = UserDefaults.standard.double(forKey: "ShotScreen_ForceCPU_StartTime") - let currentTime = Date().timeIntervalSince1970 - let elapsed = currentTime - startTime - - // Force CPU mode for 60 seconds after cache clear - if elapsed > 60 { - // Temp mode expired, clear flags - UserDefaults.standard.removeObject(forKey: "ShotScreen_ForceCPU_TempMode") - UserDefaults.standard.removeObject(forKey: "ShotScreen_ForceCPU_StartTime") - print("๐Ÿ”ง TEMPORARY CPU MODE: Expired, returning to normal mode") - return false - } - - print("๐Ÿ”ง TEMPORARY CPU MODE: Still active (\(Int(60-elapsed))s remaining)") - return true - } - - - - // MARK: - Vision Framework Processing (OPTIMIZED) - private func processWithVisionFramework(cgImage: CGImage, completion: @escaping (NSImage?) -> Void) { - guard #available(macOS 14.0, *) else { - print("โŒ Vision framework background removal requires macOS 14.0 or higher") - DispatchQueue.main.async { completion(nil) } - return - } - - print("๐Ÿš€ VISION FAST TRACK: Using optimized Vision Framework") - - let request = VNGenerateForegroundInstanceMaskRequest { request, error in - if let error = error { - print("โŒ Vision error: \(error)") - DispatchQueue.main.async { completion(nil) } - return - } - - guard let results = request.results, !results.isEmpty else { - print("โŒ No foreground found with Vision Framework") - DispatchQueue.main.async { completion(nil) } - return - } - - guard let result = results.first as? VNInstanceMaskObservation else { - print("โŒ Invalid result from Vision Framework") - DispatchQueue.main.async { completion(nil) } - return - } - - print("โœ… VISION FAST: Completed in ~1-2 seconds! (\(results.count) results)") - self.applyMask(mask: result, to: cgImage, completion: completion) - } - - // OPTIMIZATION: Configure Vision Framework for maximum speed - request.revision = VNGenerateForegroundInstanceMaskRequest.defaultRevision - - // Create handler with optimized options - let handlerOptions: [VNImageOption: Any] = [ - .ciContext: CIContext(options: [ - .useSoftwareRenderer: false, // Use hardware acceleration - .priorityRequestLow: false // High priority processing - ]) - ] - - let handler = VNImageRequestHandler(cgImage: cgImage, options: handlerOptions) - - // Perform on high-priority queue for faster processing - DispatchQueue.global(qos: .userInitiated).async { - do { - try handler.perform([request]) - } catch { - print("โŒ Vision handler error: \(error)") - DispatchQueue.main.async { completion(nil) } - } - } - } - - @available(macOS 14.0, *) - private func applyMask(mask: VNInstanceMaskObservation, to image: CGImage, completion: @escaping (NSImage?) -> Void) { - // OPTIMIZATION: Run mask processing on background queue for better performance - DispatchQueue.global(qos: .userInitiated).async { - do { - print("๐Ÿš€ FAST MASK: Starting optimized mask generation...") - - // BOTTLENECK 1: Optimize mask generation with performance options - let imageHandler = VNImageRequestHandler(cgImage: image, options: [ - .ciContext: self.getOptimizedCIContext() - ]) - - let maskImage = try mask.generateScaledMaskForImage( - forInstances: mask.allInstances, - from: imageHandler - ) - - print("๐Ÿš€ FAST MASK: Mask generated, applying to image...") - - // OPTIMIZATION: Use same optimized CIContext for all operations - let optimizedContext = self.getOptimizedCIContext() - - let ciImage = CIImage(cgImage: image) - let ciMask = CIImage(cvPixelBuffer: maskImage) - - let filter = CIFilter.blendWithMask() - filter.inputImage = ciImage - filter.backgroundImage = CIImage.empty() - filter.maskImage = ciMask - - guard let outputImage = filter.outputImage else { - print("โŒ Error applying mask") - DispatchQueue.main.async { completion(nil) } - return - } - - // BOTTLENECK 2: Use optimized context instead of creating new one - guard let cgResult = optimizedContext.createCGImage(outputImage, from: outputImage.extent) else { - print("โŒ Error creating result image") - DispatchQueue.main.async { completion(nil) } - return - } - - let resultImage = NSImage(cgImage: cgResult, size: NSSize(width: cgResult.width, height: cgResult.height)) - - DispatchQueue.main.async { - print("โœ… VISION OPTIMIZED: Background removed in ~1-2 seconds!") - completion(resultImage) - } - - } catch { - print("โŒ Error processing mask: \(error.localizedDescription)") - DispatchQueue.main.async { completion(nil) } - } - } - } - - // MARK: - Optimized CIContext (PERFORMANCE CRITICAL) - private var _optimizedCIContext: CIContext? - private func getOptimizedCIContext() -> CIContext { - if let existingContext = _optimizedCIContext { - return existingContext - } - - // Create high-performance CIContext with optimized settings - let context = CIContext(options: [ - .useSoftwareRenderer: false, // Force hardware acceleration - .priorityRequestLow: false, // High priority processing - .cacheIntermediates: true // Cache for better performance - ]) - - _optimizedCIContext = context - print("๐Ÿš€ FAST CONTEXT: Created optimized CIContext for Vision Framework") - return context - } - - // MARK: - Core ML Processing with Fallback - private func processWithCoreMLModelWithFallback(cgImage: CGImage, completion: @escaping (NSImage?) -> Void) { - // Try RMBG-1.4 first, fallback to Vision Framework - print("๐Ÿค– Attempting RMBG-1.4 Core ML model...") - - if tryLoadAndProcessModel(cgImage: cgImage, modelName: "bria-rmbg-coreml", displayName: "RMBG-1.4", completion: completion) { - // RMBG-1.4 succeeded - return - } - - // Fallback to Vision Framework - print("๐Ÿ”„ RMBG-1.4 failed, falling back to Vision Framework...") - processWithVisionFramework(cgImage: cgImage, completion: completion) - } - - private func tryLoadAndProcessModel(cgImage: CGImage, modelName: String, displayName: String, completion: @escaping (NSImage?) -> Void) -> Bool { - // Check if model is available in Application Support directory - let modelPath = getModelPath() - guard FileManager.default.fileExists(atPath: modelPath) else { - print("โŒ \(displayName) model not found at: \(modelPath)") - return false - } - - let finalModelURL = URL(fileURLWithPath: modelPath) - - do { - print("๐Ÿ“ฆ Trying to load \(displayName) model from: \(finalModelURL.lastPathComponent)") - - // First try to compile the model if it's not compiled - let compiledModelURL: URL - if finalModelURL.pathExtension == "mlpackage" { - print("๐Ÿ”ง Compiling \(displayName) model...") - compiledModelURL = try MLModel.compileModel(at: finalModelURL) - print("โœ… Model compiled successfully") - } else { - compiledModelURL = finalModelURL - } - - // Try to load the Core ML model with retry logic for E5RT issues - let model = try loadModelWithRetry(at: compiledModelURL) - let visionModel = try VNCoreMLModel(for: model) - - print("โœ… \(displayName) model successfully loaded") - - // Create the request with E5RT monitoring - let request = VNCoreMLRequest(model: visionModel) { request, error in - if let error = error { - print("โŒ Core ML request error for \(displayName): \(error)") - - // Check if this is an E5RT related error - if error.localizedDescription.contains("resources.bin") || - error.localizedDescription.contains("E5RT") { - self.recordE5RTIssue() - } - - DispatchQueue.main.async { completion(nil) } - return - } - - guard let results = request.results, - let pixelBufferObservation = results.first as? VNPixelBufferObservation else { - print("โŒ No valid result from \(displayName) model") - DispatchQueue.main.async { completion(nil) } - return - } - - // Convert the result to NSImage - if let resultImage = self.convertPixelBufferToNSImage(pixelBufferObservation.pixelBuffer, originalImage: cgImage) { - print("โœ… \(displayName) processing successful") - DispatchQueue.main.async { completion(resultImage) } - } else { - print("โŒ Could not convert \(displayName) result") - DispatchQueue.main.async { completion(nil) } - } - } - - // Configure request - request.imageCropAndScaleOption = .scaleFill - - // Perform the request - let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) - try handler.perform([request]) - - return true // Success - - } catch { - print("โŒ \(displayName) model error: \(error)") - let errorMessage = error.localizedDescription - - if errorMessage.contains("resources.bin") { - print("๐Ÿ”ง E5RT cache corruption detected - resources.bin missing") - print("๐Ÿš€ FAST FALLBACK: Immediately switching to Vision Framework") - - // Record this issue and clear cache for next time - recordE5RTIssue() - clearCoreMLCache() - - // Instead of retrying RMBG, immediately fall back to Vision - DispatchQueue.global(qos: .userInitiated).async { - self.processWithVisionFramework(cgImage: cgImage, completion: completion) - } - return true // We're handling it with Vision fallback - - } else if errorMessage.contains("MPSGraphExecutable") || errorMessage.contains("E5RT") { - print("๐Ÿ”ง Neural Engine compilation problem detected") - print("๐Ÿš€ FAST FALLBACK: Immediately switching to Vision Framework") - - // Same fast fallback strategy - recordE5RTIssue() - clearCoreMLCache() - - DispatchQueue.global(qos: .userInitiated).async { - self.processWithVisionFramework(cgImage: cgImage, completion: completion) - } - return true // We're handling it with Vision fallback - - } else if errorMessage.contains("compute") || errorMessage.contains("Neural") { - print("๐Ÿ”ง Intel Mac detected - trying CPU-only fallback") - // Try CPU-only as final fallback for Intel Macs - return tryLoadModelWithCPUOnly(cgImage: cgImage, modelName: modelName, displayName: displayName, completion: completion) - } - return false // Failed - } - } - - private func convertPixelBufferToNSImage(_ pixelBuffer: CVPixelBuffer, originalImage: CGImage) -> NSImage? { - print("๐Ÿš€ FAST RMBG: Starting optimized pixel buffer conversion...") - - // Convert the grayscale mask to a proper background-removed image - let maskCIImage = CIImage(cvPixelBuffer: pixelBuffer) - let originalCIImage = CIImage(cgImage: originalImage) - - // Resize mask to match original image size - let scaleX = originalCIImage.extent.width / maskCIImage.extent.width - let scaleY = originalCIImage.extent.height / maskCIImage.extent.height - - let scaledMask = maskCIImage.transformed(by: CGAffineTransform(scaleX: scaleX, y: scaleY)) - - // Apply the mask to remove background - let maskFilter = CIFilter.blendWithMask() - maskFilter.inputImage = originalCIImage - maskFilter.backgroundImage = CIImage.empty() - maskFilter.maskImage = scaledMask - - guard let outputImage = maskFilter.outputImage else { - print("โŒ Mask filter failed") - return nil - } - - // OPTIMIZATION: Use same optimized context instead of creating new one - let optimizedContext = getOptimizedCIContext() - guard let cgResult = optimizedContext.createCGImage(outputImage, from: outputImage.extent) else { - print("โŒ CGImage creation failed") - return nil - } - - print("โœ… RMBG OPTIMIZED: Pixel buffer converted efficiently!") - return NSImage(cgImage: cgResult, size: NSSize(width: cgResult.width, height: cgResult.height)) - } - - // MARK: - Intel Mac CPU-Only Fallback - private func tryLoadModelWithCPUOnly(cgImage: CGImage, modelName: String, displayName: String, completion: @escaping (NSImage?) -> Void) -> Bool { - let modelPath = getModelPath() - guard FileManager.default.fileExists(atPath: modelPath) else { - print("โŒ \(displayName) model not found for CPU fallback") - return false - } - - let finalModelURL = URL(fileURLWithPath: modelPath) - - do { - print("๐Ÿ”ง Trying CPU-only mode for \(displayName) on Intel Mac...") - - // Compile model if needed - let compiledModelURL: URL - if finalModelURL.pathExtension == "mlpackage" { - compiledModelURL = try MLModel.compileModel(at: finalModelURL) - } else { - compiledModelURL = finalModelURL - } - - // CPU-only configuration for Intel Macs with retry logic - print("โšก Using CPU-only compute units for Intel Mac compatibility") - - let model = try loadModelWithCPUOnly(at: compiledModelURL) - let visionModel = try VNCoreMLModel(for: model) - - print("โœ… \(displayName) model loaded successfully in CPU-only mode") - - // Create the request - let request = VNCoreMLRequest(model: visionModel) { request, error in - if let error = error { - print("โŒ CPU-only Core ML request error for \(displayName): \(error)") - DispatchQueue.main.async { completion(nil) } - return - } - - guard let results = request.results, - let pixelBufferObservation = results.first as? VNPixelBufferObservation else { - print("โŒ No valid result from \(displayName) model (CPU-only)") - DispatchQueue.main.async { completion(nil) } - return - } - - // Convert the result to NSImage - if let resultImage = self.convertPixelBufferToNSImage(pixelBufferObservation.pixelBuffer, originalImage: cgImage) { - print("โœ… \(displayName) processing successful (CPU-only mode)") - DispatchQueue.main.async { completion(resultImage) } - } else { - print("โŒ Could not convert \(displayName) result (CPU-only)") - DispatchQueue.main.async { completion(nil) } - } - } - - // Configure request - request.imageCropAndScaleOption = .scaleFill - - // Perform the request - let handler = VNImageRequestHandler(cgImage: cgImage, options: [:]) - try handler.perform([request]) - - return true // Success - - } catch { - print("โŒ CPU-only fallback failed for \(displayName): \(error)") - return false // Failed - } - } -} - -// MARK: - Drag & Drop Image View -class DragDropImageView: NSImageView, NSFilePromiseProviderDelegate, NSDraggingSource { - weak var dragDropDelegate: ImageDragDropDelegate? - var enableDragOut: Bool = false // Enable dragging images out of this view - - override func awakeFromNib() { - super.awakeFromNib() - setupDragDrop() - } - - private func setupDragDrop() { - registerForDraggedTypes([.fileURL]) - } - - // MARK: - Drag Out Functionality - override func mouseDown(with event: NSEvent) { - guard enableDragOut, let image = self.image else { - super.mouseDown(with: event) - return - } - - // Start drag operation with smaller thumbnail - let dragItem = NSDraggingItem(pasteboardWriter: image) - - // Create smaller drag frame (e.g., 120x120 max) - let maxDragSize: CGFloat = 120 - let imageSize = image.size - let aspectRatio = imageSize.width / imageSize.height - - var dragWidth: CGFloat - var dragHeight: CGFloat - - if aspectRatio > 1 { - // Landscape - dragWidth = min(maxDragSize, imageSize.width) - dragHeight = dragWidth / aspectRatio - } else { - // Portrait or square - dragHeight = min(maxDragSize, imageSize.height) - dragWidth = dragHeight * aspectRatio - } - - // Center the drag frame within the view - let dragFrame = NSRect( - x: (self.bounds.width - dragWidth) / 2, - y: (self.bounds.height - dragHeight) / 2, - width: dragWidth, - height: dragHeight - ) - - dragItem.setDraggingFrame(dragFrame, contents: image) - - // Create temporary file for dragging - if let tiffData = image.tiffRepresentation, - let bitmapRep = NSBitmapImageRep(data: tiffData), - let pngData = bitmapRep.representation(using: .png, properties: [:]) { - - let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("background_removed_\(UUID().uuidString).png") - - do { - try pngData.write(to: tempURL) - let filePromise = NSFilePromiseProvider(fileType: "public.png", delegate: self) - filePromise.userInfo = ["tempURL": tempURL] - - let fileDragItem = NSDraggingItem(pasteboardWriter: filePromise) - fileDragItem.setDraggingFrame(dragFrame, contents: image) - - beginDraggingSession(with: [fileDragItem], event: event, source: self) - - print("๐ŸŽฏ Started dragging background-removed image") - } catch { - print("โŒ Failed to create temp file for dragging: \(error)") - super.mouseDown(with: event) - } - } else { - super.mouseDown(with: event) - } - } - - override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { - let pasteboard = sender.draggingPasteboard - guard let types = pasteboard.types else { return [] } - - if types.contains(.fileURL) { - if let urls = pasteboard.readObjects(forClasses: [NSURL.self], options: nil) as? [URL] { - let imageTypes = ["png", "jpg", "jpeg", "gif", "tiff", "bmp", "heic", "heif"] - for url in urls { - if imageTypes.contains(url.pathExtension.lowercased()) { - // Visual feedback - use adaptive color - self.layer?.borderWidth = 2 - self.layer?.borderColor = ThemeManager.shared.buttonTintColor.cgColor - return .copy - } - } - } - } - return [] - } - - override func draggingExited(_ sender: NSDraggingInfo?) { - // Remove visual feedback - self.layer?.borderWidth = 0 - } - - override func performDragOperation(_ sender: NSDraggingInfo) -> Bool { - let pasteboard = sender.draggingPasteboard - guard let urls = pasteboard.readObjects(forClasses: [NSURL.self], options: nil) as? [URL] else { - return false - } - - let imageTypes = ["png", "jpg", "jpeg", "gif", "tiff", "bmp", "heic", "heif"] - for url in urls { - if imageTypes.contains(url.pathExtension.lowercased()) { - if let image = NSImage(contentsOf: url) { - dragDropDelegate?.didDropImage(image, from: url.path) - // Remove visual feedback - self.layer?.borderWidth = 0 - return true - } - } - } - return false - } - - // MARK: - NSFilePromiseProviderDelegate - func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String { - return "background_removed_\(UUID().uuidString).png" - } - - func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: @escaping (Error?) -> Void) { - guard let userInfo = filePromiseProvider.userInfo as? [String: Any], - let tempURL = userInfo["tempURL"] as? URL else { - completionHandler(NSError(domain: "DragDropImageView", code: 1, userInfo: [NSLocalizedDescriptionKey: "No temp URL found"])) - return - } - - do { - if FileManager.default.fileExists(atPath: url.path) { - try FileManager.default.removeItem(at: url) - } - try FileManager.default.copyItem(at: tempURL, to: url) - completionHandler(nil) - print("โœ… Successfully wrote dragged image to: \(url.lastPathComponent)") - } catch { - completionHandler(error) - print("โŒ Failed to write dragged image: \(error)") - } - } - - // MARK: - NSDraggingSource - func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation { - return .copy - } - - func draggingSession(_ session: NSDraggingSession, willBeginAt screenPoint: NSPoint) { - print("๐ŸŽฏ Drag session beginning at \(screenPoint)") - } - - func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) { - print("๐ŸŽฏ Drag session ended with operation: \(operation.rawValue)") - } -} - -protocol ImageDragDropDelegate: AnyObject { - func didDropImage(_ image: NSImage, from path: String) -} - -// MARK: - Card Style Button (for method selection) -class CardStyleButton: NSView { - private let titleLabel: NSTextField = NSTextField(labelWithString: "") - private let subtitleLabel: NSTextField = NSTextField(labelWithString: "") - private let iconImageView: NSImageView = NSImageView() - private var iconBackground: NSView! - private var cardBackground: NSView! - private var symbolName: String = "" - - var isSelected: Bool = false { - didSet { updateSelectedState(animated: true) } - } - - var isEnabled: Bool = true { - didSet { updateEnabledState() } - } - - // Button action properties - var target: AnyObject? - var action: Selector? - - init(frame frameRect: NSRect, title: String, subtitle: String, symbolName: String) { - self.symbolName = symbolName - super.init(frame: frameRect) - setupCard(title: title, subtitle: subtitle, symbolName: symbolName) - - // Setup theme change observer - ThemeManager.shared.observeThemeChanges { [weak self] in - DispatchQueue.main.async { - self?.updateThemeColors() - } - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupCard(title: String, subtitle: String, symbolName: String) { - wantsLayer = true - layer?.cornerRadius = 12 - layer?.masksToBounds = false - - // Mouse tracking - updateTrackingAreas() - - // Card background with blur effect - adaptive colors - cardBackground = NSView() - cardBackground.wantsLayer = true - cardBackground.translatesAutoresizingMaskIntoConstraints = false - cardBackground.layer?.cornerRadius = 12 - cardBackground.layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.3).cgColor - cardBackground.layer?.borderWidth = 1 - cardBackground.layer?.borderColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.2).cgColor - addSubview(cardBackground) - - // Icon background - adaptive colors - iconBackground = NSView() - iconBackground.wantsLayer = true - iconBackground.translatesAutoresizingMaskIntoConstraints = false - iconBackground.layer?.cornerRadius = 20 - iconBackground.layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.cgColor - cardBackground.addSubview(iconBackground) - - // SF Symbol icon - let baseImage = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil) - let configuredImage = baseImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 20, weight: .semibold)) - iconImageView.image = configuredImage - iconImageView.contentTintColor = ThemeManager.shared.primaryTextColor - iconImageView.translatesAutoresizingMaskIntoConstraints = false - iconImageView.imageScaling = .scaleProportionallyDown - iconImageView.imageAlignment = .alignCenter - iconBackground.addSubview(iconImageView) - - // Title label - adaptive colors - titleLabel.stringValue = title - titleLabel.font = NSFont.systemFont(ofSize: 12, weight: .bold) - titleLabel.textColor = ThemeManager.shared.primaryTextColor - titleLabel.alignment = .left - titleLabel.translatesAutoresizingMaskIntoConstraints = false - titleLabel.usesSingleLineMode = true - cardBackground.addSubview(titleLabel) - - // Subtitle label - adaptive colors - subtitleLabel.stringValue = subtitle - subtitleLabel.font = NSFont.systemFont(ofSize: 9, weight: .medium) - subtitleLabel.textColor = ThemeManager.shared.secondaryTextColor - subtitleLabel.alignment = .left - subtitleLabel.translatesAutoresizingMaskIntoConstraints = false - subtitleLabel.usesSingleLineMode = true - cardBackground.addSubview(subtitleLabel) - - // Layout constraints - NSLayoutConstraint.activate([ - // Card background fills the entire view - cardBackground.topAnchor.constraint(equalTo: topAnchor), - cardBackground.leadingAnchor.constraint(equalTo: leadingAnchor), - cardBackground.trailingAnchor.constraint(equalTo: trailingAnchor), - cardBackground.bottomAnchor.constraint(equalTo: bottomAnchor), - - // Icon background - iconBackground.leadingAnchor.constraint(equalTo: cardBackground.leadingAnchor, constant: 12), - iconBackground.centerYAnchor.constraint(equalTo: cardBackground.centerYAnchor), - iconBackground.widthAnchor.constraint(equalToConstant: 40), - iconBackground.heightAnchor.constraint(equalToConstant: 40), - - // Icon image - iconImageView.centerXAnchor.constraint(equalTo: iconBackground.centerXAnchor), - iconImageView.centerYAnchor.constraint(equalTo: iconBackground.centerYAnchor), - iconImageView.widthAnchor.constraint(equalToConstant: 24), - iconImageView.heightAnchor.constraint(equalToConstant: 24), - - // Title label - titleLabel.leadingAnchor.constraint(equalTo: iconBackground.trailingAnchor, constant: 12), - titleLabel.trailingAnchor.constraint(equalTo: cardBackground.trailingAnchor, constant: -12), - titleLabel.topAnchor.constraint(equalTo: cardBackground.centerYAnchor, constant: -12), - - // Subtitle label - subtitleLabel.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), - subtitleLabel.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), - subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 2) - ]) - - // Initial shadow - adaptive colors - cardBackground.layer?.shadowColor = ThemeManager.shared.shadowColor.cgColor - cardBackground.layer?.shadowOffset = CGSize(width: 0, height: 2) - cardBackground.layer?.shadowRadius = 8 - cardBackground.layer?.shadowOpacity = 0.0 - - // Set anchor points for smooth scaling - cardBackground.layer?.anchorPoint = CGPoint(x: 0.5, y: 0.5) - iconBackground.layer?.anchorPoint = CGPoint(x: 0.5, y: 0.5) - } - - override func updateTrackingAreas() { - super.updateTrackingAreas() - - for trackingArea in trackingAreas { - removeTrackingArea(trackingArea) - } - - let trackingArea = NSTrackingArea(rect: bounds, - options: [.mouseEnteredAndExited, .activeAlways], - owner: self, - userInfo: nil) - addTrackingArea(trackingArea) - } - - private func updateSelectedState(animated: Bool) { - if animated { - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.6 - context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) - - if isSelected { - // Selected state: brighter, glowing - adaptive colors - cardBackground.animator().layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.6).cgColor - cardBackground.animator().layer?.borderColor = ThemeManager.shared.buttonTintColor.withAlphaComponent(0.8).cgColor - cardBackground.animator().layer?.shadowOpacity = ThemeManager.shared.shadowOpacity - iconBackground.animator().layer?.backgroundColor = ThemeManager.shared.buttonTintColor.withAlphaComponent(0.6).cgColor - titleLabel.animator().textColor = ThemeManager.shared.primaryTextColor - subtitleLabel.animator().textColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.9) - } else { - // Unselected state: dimmer - adaptive colors - cardBackground.animator().layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.3).cgColor - cardBackground.animator().layer?.borderColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.2).cgColor - cardBackground.animator().layer?.shadowOpacity = 0.0 - iconBackground.animator().layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.cgColor - titleLabel.animator().textColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.8) - subtitleLabel.animator().textColor = ThemeManager.shared.secondaryTextColor - } - } - - // Separate animation for icon bounce - if isSelected { - let bounceAnimation = CAKeyframeAnimation(keyPath: "transform.scale") - bounceAnimation.values = [1.0, 1.2, 1.0] - bounceAnimation.keyTimes = [0.0, 0.3, 1.0] - bounceAnimation.duration = 0.5 - bounceAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) - iconBackground.layer?.add(bounceAnimation, forKey: "bounce") - } - } else { - // Immediate update without animation - adaptive colors - if isSelected { - cardBackground.layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.6).cgColor - cardBackground.layer?.borderColor = ThemeManager.shared.buttonTintColor.withAlphaComponent(0.8).cgColor - cardBackground.layer?.shadowOpacity = ThemeManager.shared.shadowOpacity - iconBackground.layer?.backgroundColor = ThemeManager.shared.buttonTintColor.withAlphaComponent(0.6).cgColor - titleLabel.textColor = ThemeManager.shared.primaryTextColor - subtitleLabel.textColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.9) - } else { - cardBackground.layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.3).cgColor - cardBackground.layer?.borderColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.2).cgColor - cardBackground.layer?.shadowOpacity = 0.0 - iconBackground.layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.cgColor - titleLabel.textColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.8) - subtitleLabel.textColor = ThemeManager.shared.secondaryTextColor - } - } - } - - private func updateEnabledState() { - if isEnabled { - alphaValue = 1.0 - cardBackground.layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.3).cgColor - cardBackground.layer?.borderColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.2).cgColor - titleLabel.textColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.8) - subtitleLabel.textColor = ThemeManager.shared.secondaryTextColor - iconBackground.layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.cgColor - iconImageView.contentTintColor = ThemeManager.shared.primaryTextColor - } else { - alphaValue = 0.5 - cardBackground.layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.1).cgColor - cardBackground.layer?.borderColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.1).cgColor - titleLabel.textColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.4) - subtitleLabel.textColor = ThemeManager.shared.secondaryTextColor.withAlphaComponent(0.5) - iconBackground.layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.withAlphaComponent(0.5).cgColor - iconImageView.contentTintColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.5) - } - updateTrackingAreas() - } - - override func mouseEntered(with event: NSEvent) { - guard isEnabled else { return } - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.2 - context.timingFunction = CAMediaTimingFunction(name: .easeOut) - - // Hover effect: slight scale and enhanced glow - cardBackground.animator().layer?.transform = CATransform3DMakeScale(1.02, 1.02, 1.0) - - if isSelected { - cardBackground.animator().layer?.shadowOpacity = ThemeManager.shared.shadowOpacity * 1.5 - } else { - cardBackground.animator().layer?.shadowOpacity = ThemeManager.shared.shadowOpacity * 0.8 - cardBackground.animator().layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.4).cgColor - } - } - - // Icon hover animation - let pulseAnimation = CABasicAnimation(keyPath: "transform.scale") - pulseAnimation.fromValue = 1.0 - pulseAnimation.toValue = 1.1 - pulseAnimation.duration = 0.15 - pulseAnimation.autoreverses = true - iconBackground.layer?.add(pulseAnimation, forKey: "pulse") - } - - override func mouseExited(with event: NSEvent) { - guard isEnabled else { return } - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.2 - context.timingFunction = CAMediaTimingFunction(name: .easeOut) - - // Reset hover effect - cardBackground.animator().layer?.transform = CATransform3DIdentity - - if isSelected { - cardBackground.animator().layer?.shadowOpacity = ThemeManager.shared.shadowOpacity - } else { - cardBackground.animator().layer?.shadowOpacity = 0.0 - cardBackground.animator().layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.3).cgColor - } - } - - iconBackground.layer?.removeAnimation(forKey: "pulse") - } - - override func mouseDown(with event: NSEvent) { - guard isEnabled else { - // Show a subtle "disabled" feedback - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.1 - self.animator().alphaValue = 0.3 - }) { - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.1 - self.animator().alphaValue = 0.5 - }) - } - return - } - - // Click animation - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.1 - cardBackground.animator().layer?.transform = CATransform3DMakeScale(0.98, 0.98, 1.0) - }) { - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.1 - self.cardBackground.animator().layer?.transform = CATransform3DMakeScale(1.02, 1.02, 1.0) - }) - } - - // Handle click action - if let target = target, let action = action { - _ = target.perform(action, with: self) - } - } - - // MARK: - Theme Management - private func updateThemeColors() { - // Update card background - if isSelected { - cardBackground.layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.6).cgColor - cardBackground.layer?.borderColor = ThemeManager.shared.buttonTintColor.withAlphaComponent(0.8).cgColor - iconBackground.layer?.backgroundColor = ThemeManager.shared.buttonTintColor.withAlphaComponent(0.6).cgColor - titleLabel.textColor = ThemeManager.shared.primaryTextColor - subtitleLabel.textColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.9) - } else { - cardBackground.layer?.backgroundColor = ThemeManager.shared.secondaryContainerBackground.withAlphaComponent(0.3).cgColor - cardBackground.layer?.borderColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.2).cgColor - iconBackground.layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.cgColor - titleLabel.textColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.8) - subtitleLabel.textColor = ThemeManager.shared.secondaryTextColor - } - - // Update icon color - iconImageView.contentTintColor = ThemeManager.shared.primaryTextColor - - // Update shadow color - cardBackground.layer?.shadowColor = ThemeManager.shared.shadowColor.cgColor - - // Update enabled state colors if disabled - if !isEnabled { - updateEnabledState() - } - } -} - -// MARK: - Action Style Button (matches GridCellView style) -class ActionStyleButton: NSView { - private let label: NSTextField = NSTextField(labelWithString: "") - private let iconImageView: NSImageView = NSImageView() - private var iconBackground: NSView! - private var iconCenterConstraint: NSLayoutConstraint! - private var iconWidthConstraint: NSLayoutConstraint! - private var iconHeightConstraint: NSLayoutConstraint! - private var iconImageWidthConstraint: NSLayoutConstraint! - private var iconImageHeightConstraint: NSLayoutConstraint! - private let iconStartOffset: CGFloat = 30 - private var isHovered: Bool = false - private var symbolName: String = "" - var isSmallButton: Bool = false { - didSet { updateForSmallButton() } - } - - var isEnabled: Bool = true { - didSet { updateEnabledState() } - } - - // Button action properties - var target: AnyObject? - var action: Selector? - - init(frame frameRect: NSRect, text: String, symbolName: String) { - self.symbolName = symbolName - super.init(frame: frameRect) - setupButton(text: text, symbolName: symbolName) - - // Setup theme change observer - ThemeManager.shared.observeThemeChanges { [weak self] in - DispatchQueue.main.async { - self?.updateThemeColors() - } - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupButton(text: String, symbolName: String) { - wantsLayer = true - // layer?.backgroundColor = NSColor.red.withAlphaComponent(0.3).cgColor // DEBUG BG - - // Mouse tracking for hover effects - will be updated in updateTrackingAreas - updateTrackingAreas() - - layer?.cornerRadius = 0 - layer?.masksToBounds = false - - // Label setup - adaptive colors - label.stringValue = text - label.font = NSFont.systemFont(ofSize: 12, weight: .semibold) - label.textColor = ThemeManager.shared.primaryTextColor - label.alignment = .left - label.alphaValue = text.isEmpty ? 0 : 0.3 // Hidden for small buttons - label.translatesAutoresizingMaskIntoConstraints = false - label.usesSingleLineMode = true - label.lineBreakMode = .byTruncatingTail - addSubview(label) - - // Circular icon background - adaptive colors - let iconBackground = NSView() - iconBackground.wantsLayer = true - iconBackground.translatesAutoresizingMaskIntoConstraints = false - iconBackground.layer?.cornerRadius = 18 // 36ร—36 circle (will be adjusted for small buttons) - iconBackground.layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.cgColor - addSubview(iconBackground) - - // SF Symbol icon - let baseImage = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil) - // Adjusted pointSize for normal state for better proportion within the 18x18 constraint - let configuredImage = baseImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)) - iconImageView.image = configuredImage - iconImageView.contentTintColor = ThemeManager.shared.primaryTextColor - iconImageView.translatesAutoresizingMaskIntoConstraints = false - iconImageView.imageScaling = .scaleProportionallyDown - iconImageView.imageAlignment = .alignCenter - iconImageView.wantsLayer = true - iconBackground.addSubview(iconImageView) - - // Store reference to icon background for small button adjustments - self.iconBackground = iconBackground - - // Layout constraints - iconCenterConstraint = iconBackground.centerXAnchor.constraint(equalTo: centerXAnchor, constant: iconStartOffset) - iconWidthConstraint = iconBackground.widthAnchor.constraint(equalToConstant: 36) - iconHeightConstraint = iconBackground.heightAnchor.constraint(equalTo: iconBackground.widthAnchor) - - // Constraints for the iconImageView itself, to ensure it's centered and sized within iconBackground - iconImageWidthConstraint = iconImageView.widthAnchor.constraint(equalToConstant: 18) // Initial size for normal buttons - iconImageHeightConstraint = iconImageView.heightAnchor.constraint(equalToConstant: 18) // Initial size for normal buttons - - NSLayoutConstraint.activate([ - iconCenterConstraint, - iconBackground.centerYAnchor.constraint(equalTo: centerYAnchor), - iconWidthConstraint, - iconHeightConstraint, - - iconImageView.centerXAnchor.constraint(equalTo: iconBackground.centerXAnchor), - iconImageView.centerYAnchor.constraint(equalTo: iconBackground.centerYAnchor), - iconImageWidthConstraint, // Activate new constraint - iconImageHeightConstraint, // Activate new constraint - - label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12), - label.trailingAnchor.constraint(equalTo: iconBackground.leadingAnchor, constant: -8), - label.centerYAnchor.constraint(equalTo: iconBackground.centerYAnchor) - ]) - - // Initial transform - layer?.anchorPoint = CGPoint(x: 0.5, y: 0.5) - layer?.transform = CATransform3DIdentity - - // Set anchor point for icon background to center for proper scaling - // This must be done BEFORE adding constraints to prevent position jumping - iconBackground.layer?.anchorPoint = CGPoint(x: 0.5, y: 0.5) - - // Add subtle glow to label - adaptive colors - let shadow = NSShadow() - shadow.shadowColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.4) - shadow.shadowBlurRadius = 4 - shadow.shadowOffset = NSSize(width: 0, height: -1) - label.shadow = shadow - } - - override func updateTrackingAreas() { - super.updateTrackingAreas() - - // Remove existing tracking areas - for trackingArea in trackingAreas { - removeTrackingArea(trackingArea) - } - - // Add tracking area covering the entire button bounds with some extra margin for easier hovering - let expandedRect = bounds.insetBy(dx: -5, dy: -5) - let trackingArea = NSTrackingArea(rect: expandedRect, - options: [.mouseEnteredAndExited, .activeAlways], - owner: self, - userInfo: nil) - addTrackingArea(trackingArea) - } - - private func updateForSmallButton() { - if isSmallButton { - // For small buttons (info, close), center the icon and make it smaller - iconCenterConstraint.constant = 0 - label.alphaValue = 0 - iconWidthConstraint.constant = 24 // iconBackground size - iconBackground.layer?.cornerRadius = 12 - iconBackground.alphaValue = 0.8 - - // Smaller icon for small buttons - let baseImage = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil) - // pointSize 10 for 12x12 constraint seems reasonable. - let configuredImage = baseImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 10, weight: .semibold)) - iconImageView.image = configuredImage - iconImageWidthConstraint.constant = 12 // Update constraint for icon image - iconImageHeightConstraint.constant = 12 // Update constraint for icon image - - } else { // This is the NORMAL/ENABLED state for the 5 action buttons - iconCenterConstraint.constant = iconStartOffset - label.alphaValue = label.stringValue.isEmpty ? 0 : 0.3 - iconWidthConstraint.constant = 36 // iconBackground size - iconBackground.layer?.cornerRadius = 18 - iconBackground.alphaValue = 1.0 - - // Reset icon for normal buttons - let baseImage = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil) - // Adjusted pointSize for normal state for better proportion - let configuredImage = baseImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 16, weight: .semibold)) - iconImageView.image = configuredImage - iconImageWidthConstraint.constant = 18 // Reset constraint for icon image - iconImageHeightConstraint.constant = 18 // Reset constraint for icon image - } - - // Update tracking areas when button type changes - updateTrackingAreas() - } - - private func updateEnabledState() { - // Update tracking areas based on enabled state - updateTrackingAreas() - } - - func setIconScale(_ scale: CGFloat) { - // Scale the icon background (which contains the icon) with animation - NSAnimationContext.runAnimationGroup { context in - context.duration = 2.0 // <<<< INCREASED DURATION HERE - context.timingFunction = CAMediaTimingFunction(name: .easeOut) - self.iconBackground.animator().layer?.transform = CATransform3DMakeScale(scale, scale, 1.0) - } - } - - func setIconScaleImmediate(_ scale: CGFloat) { - // Scale the icon background immediately without animation - self.iconBackground.layer?.transform = CATransform3DMakeScale(scale, scale, 1.0) - } - - override func mouseEntered(with event: NSEvent) { - guard isEnabled else { return } - - if isSmallButton { - // Zoom effect for small buttons (info, close) - anchor point already set in setupButton - - // Create scale animation - let scaleAnimation = CABasicAnimation(keyPath: "transform.scale") - scaleAnimation.fromValue = 1.0 - scaleAnimation.toValue = 1.2 - scaleAnimation.duration = 0.2 - scaleAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut) - scaleAnimation.fillMode = .forwards - scaleAnimation.isRemovedOnCompletion = false - - iconBackground.layer?.add(scaleAnimation, forKey: "scaleUp") - iconBackground.layer?.transform = CATransform3DMakeScale(1.2, 1.2, 1.0) - - // Alpha animation - NSAnimationContext.runAnimationGroup { ctx in - ctx.duration = 0.2 - ctx.timingFunction = CAMediaTimingFunction(name: .easeOut) - self.iconBackground.animator().alphaValue = 1.0 - } - } else { - // Slide effect for regular buttons - let shift = (self.bounds.width / 2) - 18 - 2 - NSAnimationContext.runAnimationGroup { ctx in - ctx.duration = 0.15 - ctx.timingFunction = CAMediaTimingFunction(name: .easeOut) - self.iconCenterConstraint.animator().constant = shift - self.label.animator().alphaValue = 1.0 - } - } - } - - override func mouseExited(with event: NSEvent) { - guard isEnabled else { return } - - if isSmallButton { - // Reset zoom for small buttons - iconBackground.layer?.removeAnimation(forKey: "scaleUp") - - let scaleAnimation = CABasicAnimation(keyPath: "transform.scale") - scaleAnimation.fromValue = 1.2 - scaleAnimation.toValue = 1.0 - scaleAnimation.duration = 0.2 - scaleAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut) - scaleAnimation.fillMode = .forwards - scaleAnimation.isRemovedOnCompletion = false - - iconBackground.layer?.add(scaleAnimation, forKey: "scaleDown") - iconBackground.layer?.transform = CATransform3DIdentity - - // Alpha animation - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.2 - context.timingFunction = CAMediaTimingFunction(name: .easeOut) - self.iconBackground.animator().alphaValue = 0.8 - } - } else { - // Reset slide for regular buttons - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.15 - context.timingFunction = CAMediaTimingFunction(name: .easeOut) - self.iconCenterConstraint.animator().constant = self.iconStartOffset - self.label.animator().alphaValue = 0.3 - } - } - } - - override func mouseDown(with event: NSEvent) { - guard isEnabled else { return } - - // Visual feedback for click - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.1 - if isSmallButton { - self.iconBackground.layer?.transform = CATransform3DMakeScale(0.9, 0.9, 1.0) - } else { - self.animator().alphaValue = 0.8 - } - }) { - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.1 - if self.isSmallButton { - self.iconBackground.layer?.transform = CATransform3DMakeScale(1.2, 1.2, 1.0) - } else { - self.animator().alphaValue = 1.0 - } - }) - } - - // Handle click action - if let target = target, let action = action { - print("๐Ÿ”˜ ActionStyleButton clicked: \(symbolName)") - _ = target.perform(action, with: self) - } - } - - func setIconDisabledLook(disabled: Bool) { - if disabled { - iconImageView.alphaValue = 0.5 // Make the SF Symbol itself more transparent - } else { - iconImageView.alphaValue = 1.0 // Normal visibility for the icon - } - } - - // MARK: - Theme Management - private func updateThemeColors() { - // Update label color - label.textColor = ThemeManager.shared.primaryTextColor - - // Update icon background color - iconBackground.layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.cgColor - - // Update icon tint color - iconImageView.contentTintColor = ThemeManager.shared.primaryTextColor - - // Update label shadow - let shadow = NSShadow() - shadow.shadowColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.4) - shadow.shadowBlurRadius = 4 - shadow.shadowOffset = NSSize(width: 0, height: -1) - label.shadow = shadow - - // Update enabled state if disabled - if !isEnabled { - updateEnabledState() - } - } -} - -// MARK: - Legacy code removed - use thumbnail-based BGR workflow instead - diff --git a/ShotScreen/Sources/ButtonHoverExtension.swift b/ShotScreen/Sources/ButtonHoverExtension.swift deleted file mode 100644 index 401a3a2..0000000 --- a/ShotScreen/Sources/ButtonHoverExtension.swift +++ /dev/null @@ -1,236 +0,0 @@ -import AppKit - -// Extension voor NSButton hover effecten -extension NSButton { - private struct AssociatedKeys { - static var hoverEffectScale: UInt8 = 0 - static var originalTransform: UInt8 = 1 - static var originalAnchorPoint: UInt8 = 2 - static var originalPosition: UInt8 = 3 - static var trackingArea: UInt8 = 4 - } - - private var hoverEffectScale: CGFloat? { - get { - objc_getAssociatedObject(self, &AssociatedKeys.hoverEffectScale) as? CGFloat - } - set { - objc_setAssociatedObject(self, &AssociatedKeys.hoverEffectScale, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - } - - private var originalTransform: CATransform3D? { - get { - guard let value = objc_getAssociatedObject(self, &AssociatedKeys.originalTransform) as? NSValue else { return nil } - var transform = CATransform3DIdentity - value.getValue(&transform) - return transform - } - set { - var valueToSet: NSValue? = nil - if let transform = newValue { - valueToSet = NSValue(caTransform3D: transform) - } - objc_setAssociatedObject(self, &AssociatedKeys.originalTransform, valueToSet, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - } - - private var originalAnchorPointForHover: CGPoint? { - get { - objc_getAssociatedObject(self, &AssociatedKeys.originalAnchorPoint) as? CGPoint - } - set { - objc_setAssociatedObject(self, &AssociatedKeys.originalAnchorPoint, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - } - - private var originalPositionForHover: CGPoint? { - get { - objc_getAssociatedObject(self, &AssociatedKeys.originalPosition) as? CGPoint - } - set { - objc_setAssociatedObject(self, &AssociatedKeys.originalPosition, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - } - - private var hoverTrackingArea: NSTrackingArea? { - get { - objc_getAssociatedObject(self, &AssociatedKeys.trackingArea) as? NSTrackingArea - } - set { - objc_setAssociatedObject(self, &AssociatedKeys.trackingArea, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - } - } - - func addHoverEffect(scale: CGFloat = 1.2) { - self.wantsLayer = true - self.hoverEffectScale = scale - - if self.originalTransform == nil, let layer = self.layer { - self.originalTransform = layer.transform - } - - if self.window != nil { - self.updateTrackingAreas() - } - } - - override open func mouseEntered(with event: NSEvent) { - super.mouseEntered(with: event) - - // Check if this button uses the color change effect (our new simple hover) - if objc_getAssociatedObject(self, "useZoomColorEffect") != nil { - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.2 - context.allowsImplicitAnimation = true - self.animator().contentTintColor = NSColor.white // Bright white on hover - }) - return - } - - // Check if this button uses the new hover effect - if objc_getAssociatedObject(self, "useNewHoverEffect") != nil { - // Use new hover effect with zoom and color change - if let hoverHandler = objc_getAssociatedObject(self, "hoverHandler") as? ButtonHoverHandler { - hoverHandler.mouseEntered(with: event) - } - return - } - - // ONLY process buttons that explicitly have hover effects enabled (old zoom system) - guard let scale = self.hoverEffectScale else { - return - } - - guard let layer = self.layer else { - return - } - - // Extra safety: Check if window is still valid - guard self.window != nil else { - return - } - - if self.originalTransform == nil { - self.originalTransform = layer.transform - } - if self.originalAnchorPointForHover == nil { - self.originalAnchorPointForHover = layer.anchorPoint - } - if self.originalPositionForHover == nil { - self.originalPositionForHover = layer.position - } - - layer.anchorPoint = CGPoint(x: 0.5, y: 0.5) - if let oPos = self.originalPositionForHover, let oAP = self.originalAnchorPointForHover { - layer.position = CGPoint(x: oPos.x + (layer.anchorPoint.x - oAP.x) * layer.bounds.width, - y: oPos.y + (layer.anchorPoint.y - oAP.y) * layer.bounds.height) - } - - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.2 - context.allowsImplicitAnimation = true - layer.transform = CATransform3DScale(self.originalTransform ?? CATransform3DIdentity, scale, scale, 1) - }, completionHandler: nil) - } - - override open func mouseExited(with event: NSEvent) { - super.mouseExited(with: event) - - // Check if this button uses the color change effect (our new simple hover) - if objc_getAssociatedObject(self, "useZoomColorEffect") != nil { - let originalColor = objc_getAssociatedObject(self, "originalColor") as? NSColor ?? NSColor(white: 0.8, alpha: 1.0) - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.25 - context.allowsImplicitAnimation = true - self.animator().contentTintColor = originalColor // Restore original color - }) - return - } - - // Check if this button uses the new hover effect - if objc_getAssociatedObject(self, "useNewHoverEffect") != nil { - // Use new hover effect with zoom and color change - if let hoverHandler = objc_getAssociatedObject(self, "hoverHandler") as? ButtonHoverHandler { - hoverHandler.mouseExited(with: event) - } - return - } - - // ONLY process buttons that explicitly have hover effects enabled (old zoom system) - guard self.hoverEffectScale != nil else { - return - } - - guard let layer = self.layer else { - return - } - - // Extra safety: Check if window is still valid - guard self.window != nil else { - return - } - - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.25 - context.allowsImplicitAnimation = true - layer.transform = self.originalTransform ?? CATransform3DIdentity - }, completionHandler: { [weak self] in - // Extra safety in completion handler - guard let self = self, - let safeLayer = self.layer, - self.window != nil else { - return - } - - if let oAP = self.originalAnchorPointForHover { - safeLayer.anchorPoint = oAP - } - if let oPos = self.originalPositionForHover { - safeLayer.position = oPos - } - }) - } - - override open func viewWillMove(toSuperview newSuperview: NSView?) { - super.viewWillMove(toSuperview: newSuperview) - if newSuperview == nil, let trackingArea = self.hoverTrackingArea { - self.removeTrackingArea(trackingArea) - self.hoverTrackingArea = nil - } - } - - override open func updateTrackingAreas() { - super.updateTrackingAreas() - - // Check if this button uses the new hover effect - let hasNewHoverEffect = objc_getAssociatedObject(self, "useNewHoverEffect") - - if hasNewHoverEffect != nil { - // Don't interfere with custom tracking areas for new hover effect - return - } - - // Check if this button uses the color change effect - let hasColorEffect = objc_getAssociatedObject(self, "useZoomColorEffect") - - // Process buttons that have ANY hover effect (old system OR new color effect) - guard self.hoverEffectScale != nil || hasColorEffect != nil else { - return - } - - if let existingTrackingArea = self.hoverTrackingArea { - self.removeTrackingArea(existingTrackingArea) - } - guard self.bounds != .zero else { - return - } - - let trackingArea = NSTrackingArea(rect: self.bounds, - options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect], - owner: self, - userInfo: nil) - self.addTrackingArea(trackingArea) - self.hoverTrackingArea = trackingArea - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/Config.swift b/ShotScreen/Sources/Config.swift deleted file mode 100644 index b1da3e2..0000000 --- a/ShotScreen/Sources/Config.swift +++ /dev/null @@ -1,88 +0,0 @@ -import Foundation - -struct AppConfig { - // MARK: - PicoShare Configuration (Synology Setup) - static let picoshareBaseURL = "ps.plet.i234.me" // Je Synology domein - static let useHTTPS = true - - // MARK: - PicoShare URL (Direct Access - nginx protected) - static var modelDownloadURL: String { - // Environment variable voor development/testing - if let envURL = ProcessInfo.processInfo.environment["SHOTSCREEN_MODEL_URL"] { - return envURL - } - - // Direct URL (nginx User-Agent filtering provides security) - return "https://\(picoshareBaseURL)/-2xXXWuMFfW" - } - - private static func generatePicoShareURL() -> String { - // Tijd-component (verandert elk uur voor security) - let hourComponent = Int(Date().timeIntervalSince1970) / 3600 - - // Base64 encoded delen - โœ… CONFIGURED VOOR JOUW PICOSHARE - let serverPart = "aHR0cHM6Ly9wcy5wbGV0LmkyMzQubWUv" // https://ps.plet.i234.me/ - let filePart = "LTJ4WFhXdU1GZlc=" // -2xXXWuMFfW (jouw PicoShare ID) - - guard let serverData = Data(base64Encoded: serverPart), - let fileData = Data(base64Encoded: filePart), - let serverURL = String(data: serverData, encoding: .utf8), - let fileName = String(data: fileData, encoding: .utf8) else { - // Fallback naar directe URL als obfuscation faalt - return "https://\(picoshareBaseURL)/-2xXXWuMFfW" - } - - // XOR obfuscation met tijd-component (verandert elk uur) - let timeKey = UInt8(hourComponent % 256) - let xorKey: UInt8 = 0x5A ^ timeKey // Combineer met tijd - - let obfuscatedFileName = fileName.map { char in - let ascii = char.asciiValue ?? 0 - let xored = ascii ^ xorKey - // Zorg dat het een geldig karakter blijft - if let validChar = UnicodeScalar(Int(xored)), validChar.isASCII { - return String(Character(validChar)) - } else { - return String(char) // Behoud origineel als XOR ongeldig - } - }.joined() - - print("๐Ÿ” Generated time-based URL for hour: \(hourComponent)") - return serverURL + obfuscatedFileName - } - - // MARK: - Enhanced Security Headers for PicoShare - static var secureHeaders: [String: String] { - let timestamp = String(Int(Date().timeIntervalSince1970)) - let requestId = UUID().uuidString.prefix(12) - let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0" - let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1" - - return [ - "User-Agent": "ShotScreen/\(version) (Build \(build); macOS; Synology)", - "X-App-Version": version, - "X-Request-ID": String(requestId), - "X-Timestamp": timestamp, - "X-Client-Type": "ShotScreen-Official", - "X-Device-Type": "macOS", - "Accept": "application/octet-stream", - "Cache-Control": "no-cache" - ] - } - - // MARK: - Network Security - static var userAgent: String { - let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0" - let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1" - return "ShotScreen/\(version) (Build \(build); macOS; Synology)" - } - - static var maxDownloadSize: Int64 { - return 200 * 1024 * 1024 // 200MB max - } - - // MARK: - PicoShare URL Helper (voor handmatige setup) - static func generateDirectPicoShareURL(fileID: String) -> String { - return "https://\(picoshareBaseURL)/\(fileID)" - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/CrosshairViews.swift b/ShotScreen/Sources/CrosshairViews.swift deleted file mode 100644 index 0f1b44d..0000000 --- a/ShotScreen/Sources/CrosshairViews.swift +++ /dev/null @@ -1,379 +0,0 @@ -import AppKit - -// MARK: - Custom Crosshair View -class CrosshairView: NSView { - private var crosshairPosition: NSPoint = NSPoint.zero - private let crosshairSize: CGFloat = 20 - private let lineWidth: CGFloat = 2 - - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - self.wantsLayer = true - self.layer?.backgroundColor = NSColor.clear.cgColor - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func updateCrosshairPosition(_ position: NSPoint) { - crosshairPosition = position - needsDisplay = true - } - - override func draw(_ dirtyRect: NSRect) { - super.draw(dirtyRect) - - guard let context = NSGraphicsContext.current?.cgContext else { return } - - // Clear the view - context.clear(bounds) - - // Set crosshair color (white with black outline for visibility) - context.setStrokeColor(NSColor.white.cgColor) - context.setLineWidth(lineWidth + 1) - - // Draw black outline - context.setStrokeColor(NSColor.black.cgColor) - drawCrosshair(in: context, at: crosshairPosition, size: crosshairSize + 2) - - // Draw white crosshair - context.setStrokeColor(NSColor.white.cgColor) - context.setLineWidth(lineWidth) - drawCrosshair(in: context, at: crosshairPosition, size: crosshairSize) - } - - private func drawCrosshair(in context: CGContext, at position: NSPoint, size: CGFloat) { - let halfSize = size / 2 - - // Horizontal line - context.move(to: CGPoint(x: position.x - halfSize, y: position.y)) - context.addLine(to: CGPoint(x: position.x + halfSize, y: position.y)) - - // Vertical line - context.move(to: CGPoint(x: position.x, y: position.y - halfSize)) - context.addLine(to: CGPoint(x: position.x, y: position.y + halfSize)) - - context.strokePath() - } -} - -class CrosshairCursorView: NSView { - private var trackingArea: NSTrackingArea? - private var cursorSize: CGFloat = 24.0 - private var mouseLocation: NSPoint = .zero - - // ๐Ÿ”ง NEW: Event-driven approach instead of timer-based - private var globalMouseMonitor: Any? - private var localMouseMonitor: Any? - private var isTrackingActive: Bool = false - private var lastUpdateTime: Date = Date() - private var healthCheckTimer: Timer? - - // NIEUW: Enum voor crosshair modus - enum CrosshairMode { - case normal - case allScreensActive - } - - // NIEUW: Property om de huidige modus bij te houden - var currentMode: CrosshairMode = .normal { - didSet { - if oldValue != currentMode { - DispatchQueue.main.async { // Zorg dat UI updates op main thread gebeuren - self.needsDisplay = true - } - } - } - } - - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - self.wantsLayer = true - - // Make view transparent to mouse events - self.isHidden = true // Start hidden - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - // ๐Ÿ”ง CRITICAL: Clean up on deallocation - print("๐Ÿงน DEBUG: CrosshairCursorView deinit - cleaning up") - stopTracking() - } - - override func updateTrackingAreas() { - super.updateTrackingAreas() - - if let existingTrackingArea = trackingArea { - self.removeTrackingArea(existingTrackingArea) - } - - // Track the entire window, not just this view - if let window = self.window { - let options: NSTrackingArea.Options = [.mouseMoved, .activeAlways, .mouseEnteredAndExited] - trackingArea = NSTrackingArea(rect: window.contentView?.bounds ?? self.bounds, - options: options, - owner: self, - userInfo: nil) - - if let trackingArea = trackingArea { - window.contentView?.addTrackingArea(trackingArea) - } - } - } - - // ๐Ÿ”ง NEW: Event-driven tracking system - much more reliable! - func startTracking() { - // Stop any existing tracking first - stopTracking() - - print("๐ŸŽฏ DEBUG: Starting event-driven crosshair tracking") - - // ๐Ÿ”ง CRITICAL FIX: Only start if not already active - guard !isTrackingActive else { - print("โš ๏ธ DEBUG: Tracking already active, skipping start") - return - } - - isTrackingActive = true - self.isHidden = false - - // ๐Ÿ”ง SOLUTION: Use global mouse monitoring for INSTANT response - globalMouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged]) { [weak self] event in - guard let self = self, self.isTrackingActive else { return } - DispatchQueue.main.async { - self.handleMouseEvent(event) - } - } - - // ๐Ÿ”ง SOLUTION: Also monitor local events for when mouse is over the app - localMouseMonitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged]) { [weak self] event in - guard let self = self, self.isTrackingActive else { return event } - DispatchQueue.main.async { - self.handleMouseEvent(event) - } - return event - } - - // ๐Ÿ”ง SOLUTION: Force initial position update - DispatchQueue.main.async { [weak self] in - self?.updateToCurrentMousePosition() - } - - // ๐Ÿ”ง NEW: Start health check timer - startHealthCheck() - - print("โœ… DEBUG: Event-driven tracking started successfully") - } - - private func startHealthCheck() { - // Stop any existing health check - healthCheckTimer?.invalidate() - - // Start new health check every 0.5 seconds - healthCheckTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in - guard let self = self else { return } - - let timeSinceLastUpdate = Date().timeIntervalSince(self.lastUpdateTime) - if timeSinceLastUpdate > 1.0 && self.isTrackingActive { - print("โš ๏ธ DEBUG: Crosshair hasn't updated in \(timeSinceLastUpdate)s, forcing position update") - DispatchQueue.main.async { - self.updateToCurrentMousePosition() - } - } - } - } - - // ๐Ÿ”ง NEW: Simplified fallback position update - private func updatePositionFallback(screenLocation: NSPoint) { - // Use screen coordinates with basic offset (less precise but always works) - let targetLocation = NSPoint(x: screenLocation.x - 100, y: screenLocation.y - 100) - updatePosition(location: targetLocation) - } - - func stopTracking() { - print("๐Ÿ›‘ DEBUG: Stopping event-driven crosshair tracking") - - // ๐Ÿ”ง CRITICAL FIX: Only stop if actually tracking - guard isTrackingActive else { - print("โš ๏ธ DEBUG: Tracking not active, skipping stop") - return - } - - isTrackingActive = false - - // ๐Ÿ”ง SOLUTION: Remove event monitors cleanly - if let globalMonitor = globalMouseMonitor { - NSEvent.removeMonitor(globalMonitor) - globalMouseMonitor = nil - print("โœ… DEBUG: Global monitor removed") - } - - if let localMonitor = localMouseMonitor { - NSEvent.removeMonitor(localMonitor) - localMouseMonitor = nil - print("โœ… DEBUG: Local monitor removed") - } - - self.isHidden = true - - // ๐Ÿ”ง NEW: Stop health check timer - healthCheckTimer?.invalidate() - healthCheckTimer = nil - - print("โœ… DEBUG: Event monitors removed successfully") - } - - // ๐Ÿ”ง NEW: Event handler for mouse movements - private func handleMouseEvent(_ event: NSEvent) { - guard isTrackingActive else { return } - - // ๐Ÿ”ง ROBUSTNESS: Check if we still have a valid window - guard self.window != nil else { - print("โš ๏ธ DEBUG: No window available, stopping tracking") - stopTracking() - return - } - - updateToCurrentMousePosition() - } - - // ๐Ÿ”ง NEW: Direct mouse position update - much more reliable - private func updateToCurrentMousePosition() { - guard isTrackingActive else { return } - - // ๐Ÿ”ง ROBUSTNESS: Ensure we're on main thread - guard Thread.isMainThread else { - DispatchQueue.main.async { [weak self] in - self?.updateToCurrentMousePosition() - } - return - } - - // Get current mouse position in screen coordinates - let mouseLocationScreen = NSEvent.mouseLocation - - // Convert to our coordinate system - if let targetLocation = convertScreenToViewCoordinates(mouseLocationScreen) { - updatePosition(location: targetLocation) - } else { - print("โš ๏ธ DEBUG: Failed to convert coordinates, using fallback") - updatePositionFallback(screenLocation: mouseLocationScreen) - } - } - - // ๐Ÿ”ง NEW: Robust coordinate conversion with fallback - private func convertScreenToViewCoordinates(_ screenLocation: NSPoint) -> NSPoint? { - guard let window = self.window else { - print("โš ๏ธ DEBUG: No window available for coordinate conversion") - return nil - } - - // ๐Ÿ”ง ROBUSTNESS: Validate window is visible - guard window.isVisible else { - print("โš ๏ธ DEBUG: Window not visible for coordinate conversion") - return nil - } - - let windowLocation = window.convertPoint(fromScreen: screenLocation) - - if let superview = self.superview { - return superview.convert(windowLocation, from: nil) - } else { - return windowLocation - } - } - - func updatePosition(location: NSPoint) { - // ๐Ÿ”ง SIMPLIFIED: Basic validation and direct update - guard location.x.isFinite && location.y.isFinite else { - return - } - - // ๐Ÿ”ง SIMPLIFIED: Check if position changed significantly - let tolerance: CGFloat = 1.0 - if abs(self.mouseLocation.x - location.x) < tolerance && - abs(self.mouseLocation.y - location.y) < tolerance { - return - } - - self.mouseLocation = location - self.lastUpdateTime = Date() // Update timestamp - - // ๐Ÿ”ง SIMPLIFIED: Direct frame update (we're already on main thread from event handler) - CATransaction.begin() - CATransaction.setDisableActions(true) - - let newFrame = NSRect(x: location.x - cursorSize/2, - y: location.y - cursorSize/2, - width: cursorSize, - height: cursorSize) - - if newFrame.width > 0 && newFrame.height > 0 { - self.frame = newFrame - self.needsDisplay = true - } - - CATransaction.commit() - } - - override func mouseMoved(with event: NSEvent) { - // ๐Ÿ”ง SIMPLIFIED: Use new event-driven system - handleMouseEvent(event) - } - - override func mouseEntered(with event: NSEvent) { - // ๐Ÿ”ง SIMPLIFIED: Use new event-driven system - handleMouseEvent(event) - } - - override func mouseDragged(with event: NSEvent) { - // ๐Ÿ”ง SIMPLIFIED: Use new event-driven system - handleMouseEvent(event) - } - - override func draw(_ dirtyRect: NSRect) { - // Draw a crosshair cursor - guard let context = NSGraphicsContext.current?.cgContext else { return } - - // Standaard crosshair (wit met zwarte outline) - context.setStrokeColor(NSColor.black.cgColor) - context.setLineWidth(1.0) // Outline dikte - // Horizontale lijn (outline boven) - context.move(to: CGPoint(x: 0, y: cursorSize/2 - 1.5)) - context.addLine(to: CGPoint(x: cursorSize, y: cursorSize/2 - 1.5)) - // Horizontale lijn (outline onder) - context.move(to: CGPoint(x: 0, y: cursorSize/2 + 1.5)) - context.addLine(to: CGPoint(x: cursorSize, y: cursorSize/2 + 1.5)) - // Verticale lijn (outline links) - context.move(to: CGPoint(x: cursorSize/2 - 1.5, y: 0)) - context.addLine(to: CGPoint(x: cursorSize/2 - 1.5, y: cursorSize)) - // Verticale lijn (outline rechts) - context.move(to: CGPoint(x: cursorSize/2 + 1.5, y: 0)) - context.addLine(to: CGPoint(x: cursorSize/2 + 1.5, y: cursorSize)) - context.strokePath() - - context.setStrokeColor(NSColor.white.cgColor) - context.setLineWidth(2.0) // Dikte van witte crosshair - // Horizontale lijn (wit) - context.move(to: CGPoint(x: 0, y: cursorSize/2)) - context.addLine(to: CGPoint(x: cursorSize, y: cursorSize/2)) - // Verticale lijn (wit) - context.move(to: CGPoint(x: cursorSize/2, y: 0)) - context.addLine(to: CGPoint(x: cursorSize/2, y: cursorSize)) - context.strokePath() - - // Teken extra elementen gebaseerd op modus - if currentMode == .allScreensActive { - context.setStrokeColor(NSColor.systemBlue.withAlphaComponent(0.8).cgColor) // Lichtblauwe kleur - context.setLineWidth(2.0) - let circleRect = NSRect(x: 1, y: 1, width: cursorSize - 2, height: cursorSize - 2) // Iets kleiner dan de crosshair bounds - context.addEllipse(in: circleRect) - context.strokePath() - } - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/DesktopIconManager.swift b/ShotScreen/Sources/DesktopIconManager.swift deleted file mode 100644 index b881d0c..0000000 --- a/ShotScreen/Sources/DesktopIconManager.swift +++ /dev/null @@ -1,186 +0,0 @@ -// -// DesktopIconManager.swift -// ScreenShot -// -// Created by [Your Name/AI] on [Current Date]. -// Copyright ยฉ 2025 [Your Name/AI]. All rights reserved. -// - -import Foundation -import ScreenCaptureKit - -@available(macOS 12.3, *) -class DesktopIconManager { - - static let shared = DesktopIconManager() - - private init() { - // Private constructor for singleton - } - - func logFinderWindowDetails(windows: [SCWindow]) { - NSLog("๐Ÿ•ต๏ธโ€โ™‚๏ธ DesktopIconManager: Logging details for Finder windows...") - let finderWindows = windows.filter { $0.owningApplication?.bundleIdentifier == "com.apple.finder" } - - if finderWindows.isEmpty { - NSLog("๐Ÿ•ต๏ธโ€โ™‚๏ธ No Finder windows found in the provided list.") - return - } - - NSLog("๐Ÿ•ต๏ธโ€โ™‚๏ธ Found \(finderWindows.count) Finder windows:") - for (index, window) in finderWindows.enumerated() { - let appName = window.owningApplication?.applicationName ?? "Unknown" - let bundleID = window.owningApplication?.bundleIdentifier ?? "Unknown" - - NSLog(" --------------------------------------------------") - NSLog(" ๐Ÿ•ต๏ธโ€โ™‚๏ธ Finder Window [\(index)] Details:") - NSLog(" --------------------------------------------------") - NSLog(" Title: '\(window.title ?? "N/A")'") - NSLog(" App Name: \(appName)") - NSLog(" Bundle ID: \(bundleID)") - NSLog(" Window ID: \(window.windowID)") - NSLog(" Layer: \(window.windowLayer)") - NSLog(" Frame (SCK Coords): \(NSStringFromRect(window.frame))") - NSLog(" Is OnScreen: \(window.isOnScreen)") - if #available(macOS 13.1, *) { - NSLog(" Is Active: \(window.isActive)") - } else { - NSLog(" Is Active: (N/A on this macOS version)") - } - NSLog(" --------------------------------------------------") - } - } - - /// Checks if desktop icon hiding is currently enabled in user settings - /// - Returns: true if desktop icons should be hidden during screenshots - func isDesktopIconHidingEnabled() -> Bool { - return SettingsManager.shared.hideDesktopIconsDuringScreenshot - } - - /// Checks if desktop widget hiding is currently enabled in user settings - /// - Returns: true if desktop widgets should be hidden during screenshots - func isDesktopWidgetHidingEnabled() -> Bool { - return SettingsManager.shared.hideDesktopWidgetsDuringScreenshot - } - - /// Detects desktop widgets from available windows - /// This function identifies widgets based on various criteria such as bundle identifiers and window characteristics - /// - Parameter windows: Array of SCWindow objects to analyze - /// - Returns: Array of SCWindow objects that are likely desktop widgets - func detectDesktopWidgets(from windows: [SCWindow]) -> [SCWindow] { - guard isDesktopWidgetHidingEnabled() else { - NSLog("๐Ÿ”„ DesktopIconManager: Widget hiding is disabled, returning empty array") - return [] - } - - NSLog("๐Ÿ” DesktopIconManager: Starting widget detection...") - var potentialWidgets: [SCWindow] = [] - - // Known widget bundle identifiers (based on research) - let knownWidgetBundleIdentifiers = [ - "com.apple.controlcenter", - "com.apple.notificationcenter", - "com.apple.dashboard", - "com.apple.widget", - "com.apple.WidgetKit", - "com.apple.widgets" - ] - - // Widget-like window characteristics - let widgetWindowTitles = [ - "Widget", - "Dashboard", - "Control Center", - "Notification", - "Today" - ] - - for window in windows { - let bundleId = window.owningApplication?.bundleIdentifier ?? "" - let appName = window.owningApplication?.applicationName ?? "" - let windowTitle = window.title ?? "" - let windowLayer = window.windowLayer - - var isWidget = false - var detectionReason = "" - - // Check 1: Known widget bundle identifiers - if knownWidgetBundleIdentifiers.contains(where: { bundleId.contains($0) }) { - isWidget = true - detectionReason = "Known widget bundle ID: \(bundleId)" - } - - // Check 2: Widget-like window titles - if !isWidget && widgetWindowTitles.contains(where: { windowTitle.contains($0) }) { - isWidget = true - detectionReason = "Widget-like window title: '\(windowTitle)'" - } - - // Check 3: Window layer characteristics (widgets often appear on specific layers) - // Widgets may appear on desktop-level layers (often higher than normal windows) - if !isWidget && windowLayer > 25 && windowLayer < 1000 { - // Additional checks for this layer range - if appName.lowercased().contains("widget") || - bundleId.lowercased().contains("widget") || - windowTitle.lowercased().contains("widget") { - isWidget = true - detectionReason = "High window layer (\(windowLayer)) with widget-related naming" - } - } - - // Check 4: Window size characteristics (many widgets are small, square-ish windows) - let frame = window.frame - if !isWidget && frame.width > 0 && frame.height > 0 { - let aspectRatio = frame.width / frame.height - let area = frame.width * frame.height - - // Small to medium sized windows with square-ish aspect ratios - if area < 50000 && aspectRatio >= 0.5 && aspectRatio <= 2.0 { - if bundleId.contains("apple") && ( - windowTitle.isEmpty || - windowTitle.count < 3 || - bundleId.contains("notification") || - bundleId.contains("control") - ) { - isWidget = true - detectionReason = "Small Apple window with widget characteristics (area: \(Int(area)), ratio: \(String(format: "%.2f", aspectRatio)))" - } - } - } - - if isWidget { - potentialWidgets.append(window) - NSLog("๐Ÿ“ฑ Widget detected: \(detectionReason)") - NSLog(" App: \(appName) (\(bundleId))") - NSLog(" Title: '\(windowTitle)'") - NSLog(" Layer: \(windowLayer)") - NSLog(" Frame: \(NSStringFromRect(frame))") - } - } - - NSLog("๐Ÿ” DesktopIconManager: Found \(potentialWidgets.count) potential desktop widgets") - return potentialWidgets - } - - /// Legacy placeholder for future window filtering functionality - /// Note: The main desktop icon filtering logic is now handled in ScreenCaptureKitProvider.getDesktopIconWindows() - /// which respects the hideDesktopIconsDuringScreenshot setting from SettingsManager - func shouldExcludeWindowForDesktopIconHiding(_ window: SCWindow) -> Bool { - // Desktop icon filtering is now handled in ScreenCaptureKitProvider.getDesktopIconWindows() - // This method is kept for potential future advanced filtering logic - return false - } - - /// Determines if a window should be excluded for widget hiding - /// - Parameter window: The window to check - /// - Returns: true if the window should be excluded (i.e., it's a widget and widget hiding is enabled) - func shouldExcludeWindowForWidgetHiding(_ window: SCWindow) -> Bool { - guard isDesktopWidgetHidingEnabled() else { - return false - } - - // Use the widget detection logic to determine if this single window is a widget - let widgets = detectDesktopWidgets(from: [window]) - return !widgets.isEmpty - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/DraggableImageView.swift b/ShotScreen/Sources/DraggableImageView.swift deleted file mode 100644 index 1f8ae95..0000000 --- a/ShotScreen/Sources/DraggableImageView.swift +++ /dev/null @@ -1,213 +0,0 @@ -import AppKit - -// MARK: - DraggableImageView Protocol -protocol DraggableImageViewClickHandler: AnyObject { - func thumbnailWasClicked(image: NSImage) -} - -// MARK: - DraggableImageView -class DraggableImageView: NSImageView { - var onDragStart: (() -> Void)? - weak var appDelegate: ScreenshotApp? - private var mouseDownEvent: NSEvent? - private let dragThreshold: CGFloat = 3.0 - private var isPerformingDrag: Bool = false - - // ๐ŸŽจ NEW: Track the file URL for the current image (for BGR mode) - var imageURL: URL? - - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - self.imageScaling = .scaleProportionallyUpOrDown - self.imageAlignment = .alignCenter - self.animates = true - self.imageFrameStyle = .none - self.registerForDraggedTypes([.fileURL, .URL, .tiff, .png]) - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - self.imageScaling = .scaleProportionallyUpOrDown - self.imageAlignment = .alignCenter - self.animates = true - self.imageFrameStyle = .none - self.registerForDraggedTypes([.fileURL, .URL, .tiff, .png]) - } - - override func mouseDown(with event: NSEvent) { - self.mouseDownEvent = event - self.isPerformingDrag = false - } - - override func mouseDragged(with event: NSEvent) { - guard let mouseDownEvent = self.mouseDownEvent else { - super.mouseDragged(with: event) - return - } - - if !isPerformingDrag { - let dragThreshold: CGFloat = 3.0 - let deltaX = abs(event.locationInWindow.x - mouseDownEvent.locationInWindow.x) - let deltaY = abs(event.locationInWindow.y - mouseDownEvent.locationInWindow.y) - - if deltaX > dragThreshold || deltaY > dragThreshold { - isPerformingDrag = true - self.mouseDownEvent = nil - - guard let unwrappedAppDelegate = appDelegate else { - isPerformingDrag = false - return - } - - // ๐ŸŽจ FIXED: Check for available URL (BGR mode or normal mode) before proceeding - let sourceURLForDrag = self.imageURL ?? unwrappedAppDelegate.tempURL - - guard let finalSourceURL = sourceURLForDrag else { - print("โŒ DraggableImageView: No valid URL available for dragging (imageURL: \(imageURL?.path ?? "nil"), tempURL: \(unwrappedAppDelegate.tempURL?.path ?? "nil"))") - isPerformingDrag = false - return - } - - print("๐ŸŽฏ DraggableImageView: Starting drag with URL: \(finalSourceURL.path)") - - if let preview = unwrappedAppDelegate.activePreviewWindow, preview.isVisible { - preview.orderOut(nil as Any?) - } - unwrappedAppDelegate.gridViewManager?.showGrid(previewFrame: self.window?.frame) - - // NIEUW: Start drag session voor proximity monitoring - unwrappedAppDelegate.gridViewManager?.startDragSession() - - let fileItem = NSDraggingItem(pasteboardWriter: finalSourceURL as NSURL) - - if let imageToDrag = self.image { - let fullFrame = convert(bounds, to: nil) - - let scale: CGFloat = 0.05 - let yOffset: CGFloat = 30 - - let scaledFrame = NSRect( - x: fullFrame.midX - fullFrame.width * scale / 2, - y: fullFrame.midY - fullFrame.height * scale / 2 - yOffset, - width: fullFrame.width * scale, - height: fullFrame.height * scale - ) - fileItem.setDraggingFrame(scaledFrame, contents: imageToDrag) - } - let items: [NSDraggingItem] = [fileItem] - beginDraggingSession(with: items, event: event, source: self) - } else { - return - } - } - } - - override func mouseUp(with event: NSEvent) { - if !isPerformingDrag { - if let image = self.image, let appDelegate = self.appDelegate { - appDelegate.thumbnailWasClicked(image: image) - } - super.mouseUp(with: event) - } - self.mouseDownEvent = nil - self.isPerformingDrag = false - } -} - -// MARK: - NSDraggingSource -extension DraggableImageView: NSDraggingSource { - func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation { - return .copy - } - - func draggingSession(_ session: NSDraggingSession, willBeginAt screenPoint: NSPoint) { - // Drag session beginning - } - - func draggingSession(_ session: NSDraggingSession, movedTo screenPoint: NSPoint) { - guard let appDel = self.appDelegate, - let gridManager = appDel.gridViewManager, - let gridWindow = gridManager.gridWindow else { return } - - let gridFrame = gridWindow.frame - - let distanceToGrid = min( - abs(screenPoint.x - gridFrame.minX), - abs(screenPoint.x - gridFrame.maxX) - ) - - // Update visual feedback based on proximity - if Int(distanceToGrid) % 50 == 0 { - let minScale: CGFloat = 0.05 - let maxScale: CGFloat = 0.35 - let maxDistance: CGFloat = 300 - - if distanceToGrid < maxDistance { - let progress = distanceToGrid / maxDistance - _ = minScale + (progress * (maxScale - minScale)) - } - } - } - - func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) { - isPerformingDrag = false - - // NIEUW: Stop drag session direct na drag end - appDelegate?.gridViewManager?.stopDragSession() - - DispatchQueue.main.async { [weak self] in - guard let self = self, let appDel = self.appDelegate else { - return - } - - let didDropOnGridAction = appDel.didGridHandleDrop - let didDropOnStashGridAction = appDel.didStashGridHandleDrop - let didDropOnAnyGridAction = didDropOnGridAction || didDropOnStashGridAction - let closeAfterDragSetting = SettingsManager.shared.closeAfterDrag - - if !didDropOnAnyGridAction && appDel.gridViewManager?.gridWindow != nil { - appDel.gridViewManager?.hideGrid(monitorForReappear: false) - } - - if didDropOnAnyGridAction { - // Drop handled by grid action (main or stash). Preview management deferred to grid action handler. - print("๐Ÿ”„ DraggableImageView: Grid action detected (main: \(didDropOnGridAction), stash: \(didDropOnStashGridAction))") - - // ๐Ÿ”ง CRITICAL FIX: Handle ALL grid actions (both main and stash) with closeAfterDrag setting - print("๐Ÿ”„ Grid action completed - applying closeAfterDrag setting: \(closeAfterDragSetting)") - if closeAfterDragSetting { - print("๐Ÿ”„ Closing thumbnail after grid action due to closeAfterDrag setting") - appDel.closePreviewWithAnimation(immediate: false, preserveTempFile: false) - } else { - print("๐Ÿ”„ Keeping thumbnail visible after grid action (closeAfterDrag is OFF)") - appDel.ensurePreviewVisible() - } - } else { - if operation != [] { - if closeAfterDragSetting { - appDel.closePreviewWithAnimation(immediate: true) - } else { - appDel.ensurePreviewVisible() - } - } else { - appDel.ensurePreviewVisible() - } - } - - // Reset both flags - appDel.didGridHandleDrop = false - appDel.didStashGridHandleDrop = false - } - } -} - -// MARK: - NSImage Extension for PNG Data -extension NSImage { - func pngData() -> Data? { - guard let tiffData = self.tiffRepresentation, - let bitmapRep = NSBitmapImageRep(data: tiffData) else { - return nil - } - return bitmapRep.representation(using: .png, properties: [:]) - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/EventCapture.swift b/ShotScreen/Sources/EventCapture.swift deleted file mode 100644 index 9891a83..0000000 --- a/ShotScreen/Sources/EventCapture.swift +++ /dev/null @@ -1,280 +0,0 @@ -import AppKit - -// MARK: - Event Capture Window -class EventCaptureWindow: NSWindow { - override var canBecomeKey: Bool { - return true - } - - override var canBecomeMain: Bool { - return true - } - - override func sendEvent(_ event: NSEvent) { - // During screenshot selection, we want to capture ALL events - // and prevent them from reaching other windows - switch event.type { - case .leftMouseDown, .leftMouseUp, .leftMouseDragged, - .rightMouseDown, .rightMouseUp, .rightMouseDragged, - .otherMouseDown, .otherMouseUp, .otherMouseDragged, - .mouseMoved, .mouseEntered, .mouseExited, - .scrollWheel, .keyDown, .keyUp: - // Process the event normally through our view hierarchy - super.sendEvent(event) - // DO NOT pass the event to other windows - this prevents - // other windows from being dragged or interacted with - return - default: - // For other event types, allow normal processing - super.sendEvent(event) - } - } -} - -// MARK: - Event Capture View -class EventCaptureView: NSView { - weak var screenshotApp: ScreenshotApp? - var shouldDisplayAllScreensActiveText: Bool = false // Nieuwe property - - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - setupView() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setupView() - } - - private func setupView() { - // This view will handle mouse events - no special setup needed - self.wantsLayer = true // Zorg ervoor dat de view een layer heeft voor efficiรซnt tekenen - } - - override func draw(_ dirtyRect: NSRect) { - super.draw(dirtyRect) // Belangrijk voor standaard teken gedrag - - if shouldDisplayAllScreensActiveText { - // Teken "Alle Schermen Actief" tekst-overlay bovenaan het scherm van deze view - let instructionText = "All Screens Mode, Click to Capture, ESC to Cancel" - let attributes: [NSAttributedString.Key: Any] = [ - .font: NSFont.systemFont(ofSize: 18, weight: .medium), - .foregroundColor: NSColor.white.withAlphaComponent(0.9), - .strokeColor: NSColor.black.withAlphaComponent(0.5), // Tekst outline - .strokeWidth: -2.0, // Negatief voor outline binnen de letters - .paragraphStyle: { - let style = NSMutableParagraphStyle() - style.alignment = .center - return style - }() - ] - let attributedString = NSAttributedString(string: instructionText, attributes: attributes) - let textSize = attributedString.size() - - // self.bounds is de grootte van deze EventCaptureView, die het hele scherm beslaat. - let viewBounds = self.bounds - - let textRect = NSRect(x: (viewBounds.width - textSize.width) / 2, - y: viewBounds.height - textSize.height - 30, // 30px van de top van het scherm - width: textSize.width, - height: textSize.height) - - let backgroundPadding: CGFloat = 10 - let backgroundRect = NSRect( - x: textRect.origin.x - backgroundPadding, - y: textRect.origin.y - backgroundPadding, - width: textRect.width + (2 * backgroundPadding), - height: textRect.height + (2 * backgroundPadding) - ) - let BORDER_RADIUS: CGFloat = 10 - let textBackgroundPath = NSBezierPath(roundedRect: backgroundRect, xRadius: BORDER_RADIUS, yRadius: BORDER_RADIUS) - NSColor.black.withAlphaComponent(0.4).setFill() - textBackgroundPath.fill() - - attributedString.draw(in: textRect) - // NSLog("๐ŸŽจ EventCaptureView: Drew 'Alle Schermen Actief' text.") - } - } - - override func mouseDown(with event: NSEvent) { - guard let app = screenshotApp else { - // Even if app is nil, consume the event to prevent it from reaching other windows - return - } - - if app.isMultiMonitorSelectionActive && !app.isDragging { - let globalLocation = NSEvent.mouseLocation - print("๐ŸŽฏ Event capture - starting selection at: \(globalLocation)") - - // SET MOUSE TRACKING VARIABLES FOR SINGLE CLICK DETECTION - let allScreenModifier: UInt = (1 << 0) // Command key - app.isAllScreenModifierPressed = app.isModifierPressed(event.modifierFlags, modifier: allScreenModifier) - app.mouseDownLocation = globalLocation - app.mouseDownTime = CACurrentMediaTime() - app.hasMouseMoved = false - - - - // Hide crosshairs during drag to prevent stuck crosshair issue - hideCrosshairs() - - app.startDragSelection(at: globalLocation) - } - // DO NOT call super.mouseDown - this prevents the event from propagating - } - - override func mouseDragged(with event: NSEvent) { - guard let app = screenshotApp else { - // Even if app is nil, consume the event to prevent it from reaching other windows - return - } - - if app.isMultiMonitorSelectionActive && app.isDragging { - let globalLocation = NSEvent.mouseLocation - app.hasMouseMoved = true // MARK THAT MOUSE HAS MOVED - app.updateDragSelection(to: globalLocation) - } - // DO NOT call super.mouseDragged - this prevents the event from propagating - } - - override func mouseUp(with event: NSEvent) { - guard let app = screenshotApp else { - // Even if app is nil, consume the event to prevent it from reaching other windows - return - } - - if app.isMultiMonitorSelectionActive && app.isDragging { - let globalLocation = NSEvent.mouseLocation - - // CHECK FOR SINGLE CLICK FIRST - let timeSinceMouseDown = CACurrentMediaTime() - app.mouseDownTime - let distanceMoved = sqrt(pow(globalLocation.x - app.mouseDownLocation.x, 2) + pow(globalLocation.y - app.mouseDownLocation.y, 2)) - - print("๐ŸŽฏ EventCaptureView - mouse up at: \(globalLocation)") - print("๐ŸŽฏ Time since down: \(timeSinceMouseDown)s, distance: \(distanceMoved)px, moved: \(app.hasMouseMoved), allScreenModifierPressed: \(app.isAllScreenModifierPressed), allScreensToggled: \(app.isAllScreensCaptureToggledOn)") - - - - // 1. NIEUW: Als "alle schermen" modus is GETOGGLED AAN, en het is een klik (geen drag) - if app.isAllScreensCaptureToggledOn && !app.hasMouseMoved && distanceMoved < 5.0 && timeSinceMouseDown < 0.5 { - print("๐ŸŽฏ Click detected (toggle ON) - capturing all screens") - app.deactivateMultiMonitorSelection() - app.captureAllScreens() // captureAllScreens reset zelf de toggle - app.resetTrackingVariables() - return - } - - // 2. Prioriteit: Single click voor huidige scherm (als toggle NIET aanstaat) - if !app.hasMouseMoved && distanceMoved < 5.0 && timeSinceMouseDown < 0.5 && !app.isAllScreenModifierPressed { - print("๐ŸŽฏ Single click detected (toggle OFF) - capturing current screen") - app.deactivateMultiMonitorSelection() - app.captureCurrentScreen(at: globalLocation) - app.resetTrackingVariables() // RESET for clean state - return - } - - // 4. Anders, normale selectie beรซindigen - print("๐ŸŽฏ Ending drag selection normally") - app.endDragSelection(at: globalLocation) - - // Show crosshairs again after drag ends - showCrosshairs() - } - // DO NOT call super.mouseUp - this prevents the event from propagating - } - - override func rightMouseDown(with event: NSEvent) { - // Handle right-click to cancel selection - guard let app = screenshotApp else { return } - - if app.isMultiMonitorSelectionActive { - print("๐ŸŽฏ Event capture - right-click, canceling selection") - app.cancelMultiMonitorSelection() - } - // DO NOT call super.rightMouseDown - this prevents the event from propagating - } - - override func rightMouseUp(with event: NSEvent) { - // Consume right mouse up events during selection - // DO NOT call super.rightMouseUp - this prevents the event from propagating - } - - override func otherMouseDown(with event: NSEvent) { - // Consume other mouse button events during selection - // DO NOT call super.otherMouseDown - this prevents the event from propagating - } - - override func otherMouseUp(with event: NSEvent) { - // Consume other mouse button events during selection - // DO NOT call super.otherMouseUp - this prevents the event from propagating - } - - override func scrollWheel(with event: NSEvent) { - // Consume scroll wheel events during selection - // DO NOT call super.scrollWheel - this prevents the event from propagating - } - - override func keyDown(with event: NSEvent) { - // Only handle ESC key if multi-monitor selection is active - if event.keyCode == 53 && screenshotApp?.isMultiMonitorSelectionActive == true { // ESC key - screenshotApp?.cancelMultiMonitorSelection() - return // Consume event - } - - // The Command-key handling has been moved to the flagsChanged handler - // in MultiMonitorSystem.swift to implement the toggle correctly. - - super.keyDown(with: event) - } - - override func keyUp(with event: NSEvent) { - // ... existing code ... - super.keyUp(with: event) - } - - override var acceptsFirstResponder: Bool { - return true - } - - override func becomeFirstResponder() -> Bool { - print("โŒจ๏ธ EventCaptureView becoming first responder") - return super.becomeFirstResponder() - } - - private func hideCrosshairs() { - // Find all crosshair views in the window hierarchy and hide them - if let window = self.window { - findAndHideCrosshairs(in: window.contentView) - } - } - - private func showCrosshairs() { - // Find all crosshair views in the window hierarchy and show them - if let window = self.window { - findAndShowCrosshairs(in: window.contentView) - } - } - - private func findAndHideCrosshairs(in view: NSView?) { - guard let view = view else { return } - - for subview in view.subviews { - if let crosshair = subview as? CrosshairCursorView { - crosshair.stopTracking() - } - findAndHideCrosshairs(in: subview) - } - } - - private func findAndShowCrosshairs(in view: NSView?) { - guard let view = view else { return } - - for subview in view.subviews { - if let crosshair = subview as? CrosshairCursorView { - crosshair.isHidden = false - crosshair.startTracking() - } - findAndShowCrosshairs(in: subview) - } - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/FeedbackBubblePanel.swift b/ShotScreen/Sources/FeedbackBubblePanel.swift deleted file mode 100644 index 2cbac01..0000000 --- a/ShotScreen/Sources/FeedbackBubblePanel.swift +++ /dev/null @@ -1,163 +0,0 @@ -import AppKit - -class FeedbackBubblePanel: NSPanel { - private var messageLabel: NSTextField! - private var animationStartFrame: NSRect? - private var autoCloseTimer: Timer? - - // MARK: - Initialization - init(contentRect: NSRect, text: String) { - super.init(contentRect: contentRect, styleMask: [.borderless, .utilityWindow, .hudWindow, .nonactivatingPanel], backing: .buffered, defer: false) - - self.isFloatingPanel = true - self.level = .floating + 3 - self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] - self.isOpaque = false - self.backgroundColor = .clear - self.hasShadow = false - self.animationBehavior = .utilityWindow - self.hidesOnDeactivate = false // Belangrijk voor interactie met andere UI tijdens tonen - self.becomesKeyOnlyIfNeeded = true - - setupVisualEffectView() - setupMessageLabel(with: text) - setupLayoutConstraints() - } - - private func setupVisualEffectView() { - let visualEffectView = NSVisualEffectView() - visualEffectView.blendingMode = .behindWindow - visualEffectView.material = .hudWindow - visualEffectView.state = .active - visualEffectView.wantsLayer = true - visualEffectView.layer?.cornerRadius = 12.0 - visualEffectView.translatesAutoresizingMaskIntoConstraints = false - self.contentView = visualEffectView - } - - private func setupMessageLabel(with text: String) { - messageLabel = NSTextField(wrappingLabelWithString: text) - messageLabel.translatesAutoresizingMaskIntoConstraints = false - messageLabel.isBezeled = false - messageLabel.isEditable = false - messageLabel.isSelectable = false - messageLabel.backgroundColor = .clear - messageLabel.textColor = NSColor(white: 0.95, alpha: 1.0) - messageLabel.font = NSFont.systemFont(ofSize: 13, weight: .medium) - messageLabel.alignment = .center - messageLabel.maximumNumberOfLines = 0 // Allow multiple lines - (self.contentView as? NSVisualEffectView)?.addSubview(messageLabel) - } - - private func setupLayoutConstraints() { - guard let contentView = self.contentView as? NSVisualEffectView, messageLabel != nil else { return } - - let horizontalPadding: CGFloat = 18 - let verticalPadding: CGFloat = 10 - - // Pas de grootte van het paneel aan op basis van de tekst, met padding - let preferredSize = messageLabel.sizeThatFits(NSSize(width: 250 - 2 * horizontalPadding, height: CGFloat.greatestFiniteMagnitude)) - let panelWidth = preferredSize.width + 2 * horizontalPadding - let panelHeight = preferredSize.height + 2 * verticalPadding - self.setContentSize(NSSize(width: panelWidth, height: panelHeight)) - - // Herbereken de constraints van de visualEffectView indien nodig - contentView.leadingAnchor.constraint(equalTo: (self.contentView!).leadingAnchor).isActive = true - contentView.trailingAnchor.constraint(equalTo: (self.contentView!).trailingAnchor).isActive = true - contentView.topAnchor.constraint(equalTo: (self.contentView!).topAnchor).isActive = true - contentView.bottomAnchor.constraint(equalTo: (self.contentView!).bottomAnchor).isActive = true - - NSLayoutConstraint.activate([ - messageLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: horizontalPadding), - messageLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -horizontalPadding), - messageLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: verticalPadding), - messageLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -verticalPadding) - ]) - } - - // MARK: - Show and Close Animations - - /// Shows the panel, animating from the grid. - func show(aroundGridFrame gridFrame: NSRect, text: String, autoCloseAfter: TimeInterval?, onAutoCloseCompletion: (() -> Void)? = nil) { - self.messageLabel.stringValue = text - // Herbereken layout voor nieuwe tekst - setupLayoutConstraints() - - self.animationStartFrame = gridFrame - let panelSize = self.frame.size - - // Startpositie: gecentreerd op de grid (vergelijkbaar met RenamePanel) - let initialX = gridFrame.midX - panelSize.width / 2 - let initialY = gridFrame.midY - panelSize.height / 2 - self.setFrameOrigin(NSPoint(x: initialX, y: initialY)) - self.alphaValue = 0.0 - - // Eindpositie: links van de grid (vergelijkbaar met RenamePanel) - let spacing: CGFloat = 20 - var finalFrame = self.frame - finalFrame.origin.x = gridFrame.origin.x - panelSize.width - spacing - finalFrame.origin.y = gridFrame.origin.y + (gridFrame.height - panelSize.height) / 2 - - // Screen bounds check (vereenvoudigd, neem aan dat het past voor nu) - if let screen = NSScreen.screens.first(where: { $0.frame.intersects(gridFrame) }) ?? NSScreen.main { - let screenVisibleFrame = screen.visibleFrame - if finalFrame.origin.x < screenVisibleFrame.origin.x { - finalFrame.origin.x = gridFrame.maxX + spacing // Aan de andere kant als het niet past - } - finalFrame.origin.y = max(screenVisibleFrame.minY + 10, min(finalFrame.origin.y, screenVisibleFrame.maxY - panelSize.height - 10)) - } - - self.orderFront(nil) - - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.35 // Iets langzamer dan rename panel (0.3) - context.timingFunction = CAMediaTimingFunction(name: .easeOut) - self.animator().alphaValue = 1.0 - self.animator().setFrame(finalFrame, display: true) - }, completionHandler: { - if let closeDelay = autoCloseAfter, closeDelay > 0 { - self.autoCloseTimer?.invalidate() - self.autoCloseTimer = Timer.scheduledTimer(withTimeInterval: closeDelay, repeats: false) { [weak self] _ in - self?.closeWithAnimation(completion: onAutoCloseCompletion) - } - } - }) - } - - /// Closes the panel, animating back towards the grid. - func closeWithAnimation(completion: (() -> Void)?) { - autoCloseTimer?.invalidate() - autoCloseTimer = nil - - guard let startFrame = self.animationStartFrame else { - self.orderOut(nil) - completion?() - return - } - - // Doel animatie: terug naar midden van grid, met huidige grootte (geen zoom/krimp) - let currentPanelSize = self.frame.size - let endOriginX = startFrame.midX - currentPanelSize.width / 2 - let endOriginY = startFrame.midY - currentPanelSize.height / 2 - // We animeren alleen de origin en alpha, niet de size. - - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.3 // Snelheid van terug animeren - context.timingFunction = CAMediaTimingFunction(name: .easeIn) - self.animator().alphaValue = 0 - // Alleen de origin animeren, niet de frame size. - self.animator().setFrameOrigin(NSPoint(x: endOriginX, y: endOriginY)) - }, completionHandler: { - self.orderOut(nil) - self.alphaValue = 1 // Reset alpha voor volgende keer - // Belangrijk: Reset ook de frame origin/size naar iets zinnigs voor het geval het direct opnieuw getoond wordt - // zonder dat de init opnieuw doorlopen wordt, hoewel dat hier minder waarschijnlijk is. - // Voor nu laten we dit, aangezien een nieuwe .show() de frame opnieuw instelt. - completion?() - }) - } - - deinit { - autoCloseTimer?.invalidate() - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/FinderWindowManager.swift b/ShotScreen/Sources/FinderWindowManager.swift deleted file mode 100644 index 76b1a51..0000000 --- a/ShotScreen/Sources/FinderWindowManager.swift +++ /dev/null @@ -1,64 +0,0 @@ -import AppKit -import Foundation - -// MARK: - Finder Window Manager -class FinderWindowManager { - - // MARK: - Data Structures - struct FinderWindowInfo { - let path: String - let bounds: NSRect - let viewType: String - let sortColumn: String - let reversed: Bool - let windowIndex: Int - - init(path: String, bounds: NSRect, viewType: String = "icon view", sortColumn: String = "name", reversed: Bool = false, windowIndex: Int = 0) { - self.path = path - self.bounds = bounds - self.viewType = viewType - self.sortColumn = sortColumn - self.reversed = reversed - self.windowIndex = windowIndex - } - } - - // MARK: - Properties - private var savedWindows: [FinderWindowInfo] = [] - private var isCurrentlyRestoring: Bool = false - - // MARK: - Public Interface - - /// Save all currently open Finder windows and their properties - /// NOTE: Currently disabled - functionality removed for simplicity - func saveOpenFinderWindows() { - // Feature disabled - no window saving functionality - return - } - - /// Restore previously saved Finder windows as individual windows - /// NOTE: Currently disabled - functionality removed for simplicity - func restoreFinderWindows() { - // Feature disabled - no window restoration functionality - return - } - - /// Clear saved window data - func cleanup() { - savedWindows.removeAll() - isCurrentlyRestoring = false - print("๐Ÿงน FinderWindowManager cleaned up") - } - - /// Force cleanup - only call when app is terminating - func forceCleanup() { - cleanup() - print("๐Ÿงน FinderWindowManager force cleaned up (app terminating)") - } - - /// Update saved positions without full cleanup - useful for position tracking - func updateCurrentPositions() { - // This will refresh the saved positions with current window states - saveOpenFinderWindows() - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/FirstLaunchWizard.swift b/ShotScreen/Sources/FirstLaunchWizard.swift deleted file mode 100644 index c9cbdea..0000000 --- a/ShotScreen/Sources/FirstLaunchWizard.swift +++ /dev/null @@ -1,606 +0,0 @@ -import AppKit -import SwiftUI -import ServiceManagement - -// MARK: - Environment Key for Window Access -private struct WindowKey: EnvironmentKey { - static let defaultValue: FirstLaunchWizard? = nil -} - -extension EnvironmentValues { - var window: FirstLaunchWizard? { - get { self[WindowKey.self] } - set { self[WindowKey.self] = newValue } - } -} - -// MARK: - PreferenceKey for Button Width Sync -struct ButtonWidthPreferenceKey: PreferenceKey { - static var defaultValue: CGFloat = 0 - static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { - value = max(value, nextValue()) - } -} - -// MARK: - First Launch Wizard Window -class FirstLaunchWizard: NSWindow { - private var hostingView: NSHostingView? - private var isClosing = false - - // Perfect fit window size - made smaller - static let windowSize = NSSize(width: 420, height: 520) - - init() { - let windowRect = NSRect(origin: .zero, size: FirstLaunchWizard.windowSize) - - super.init( - contentRect: windowRect, - styleMask: [.titled, .fullSizeContentView], - backing: .buffered, - defer: false - ) - - setupWindow() - setupContent() - } - - private func setupWindow() { - title = "Welcome to ShotScreen" - isOpaque = false - backgroundColor = .clear - hasShadow = true - titleVisibility = .hidden - titlebarAppearsTransparent = true - isMovable = true - isMovableByWindowBackground = true - level = .normal - isReleasedWhenClosed = true - - // Center the window - center() - } - - private func setupContent() { - let wizardView = SinglePageWizardView { [unowned self] in - // Completion handler - mark first launch as completed - SettingsManager.shared.hasCompletedFirstLaunch = true - print("โœ… First launch wizard completed - closing window") - print("๐Ÿ” DEBUG: About to call safeClose(), self exists: \(self)") - self.safeClose() - } - - hostingView = NSHostingView(rootView: AnyView(wizardView.environment(\.window, self))) - contentView = hostingView - } - - private func safeClose() { - guard Thread.isMainThread else { - DispatchQueue.main.async { [weak self] in - self?.safeClose() - } - return - } - - guard !isClosing else { - print("๐Ÿ” DEBUG: Already closing, skipping") - return - } - isClosing = true - - print("๐ŸŽฏ Safely closing wizard...") - - // Force close immediately without async - orderOut(nil) - close() - } - - override func close() { - print("๐ŸŽ‰ FirstLaunchWizard: Window closing") - - guard !isClosing else { - print("๐Ÿ” DEBUG: Close() called but already closing") - return - } - isClosing = true - - // First hide the window immediately - orderOut(nil) - - // Clean up hosting view - if let hosting = hostingView { - hosting.rootView = AnyView(EmptyView()) - hosting.removeFromSuperview() - hostingView = nil - } - - contentView = nil - - // Force the superclass close - super.close() - - print("๐ŸŽ‰ FirstLaunchWizard: Window actually closed") - } - - deinit { - print("๐ŸŽ‰ FirstLaunchWizard: Deallocated") - } -} - -// MARK: - Single Page Wizard View -struct SinglePageWizardView: View { - let onComplete: () -> Void - @Environment(\.window) private var window - - // State for permission checks - @State private var hasScreenRecordingPermission = false - @State private var isLaunchAtStartupEnabled = false - @State private var hasCompletedScreenshotSettings = false - - var body: some View { - VStack(spacing: 0) { - // Header with logo and title - CompactHeaderView() - .padding(.top, 20) - .padding(.bottom, 20) - - // All content perfectly fitted without scrolling - VStack(spacing: 16) { - // Setup section - SetupSection( - hasScreenRecordingPermission: hasScreenRecordingPermission, - hasCompletedScreenshotSettings: hasCompletedScreenshotSettings, - onScreenshotSettingsCompleted: { - hasCompletedScreenshotSettings = true - } - ) - - // Configuration section - ConfigurationSection( - isLaunchAtStartupEnabled: isLaunchAtStartupEnabled - ) - } - .padding(.horizontal, 20) - - Spacer() - - // Bottom action buttons - HStack(spacing: 10) { - Button("Skip Setup") { - print("๐Ÿ”ด Skip Setup button pressed") - onComplete() - } - .buttonStyle(.bordered) - .foregroundColor(.secondary) - .controlSize(.small) - - Spacer() - - Button("Finished!") { - print("๐ŸŸข Finished! button pressed") - onComplete() - } - .buttonStyle(.borderedProminent) - .controlSize(.small) - } - .padding(.top, 20) - .padding(.bottom, 20) - .padding(.horizontal, 20) - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background( - VisualEffectBackground(material: .hudWindow, blending: .behindWindow) - .cornerRadius(16) - ) - .padding(14) - .onAppear(perform: checkPermissions) - .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in - print("App became active, re-checking permissions...") - checkPermissions() - } - } - - private func checkPermissions() { - // Check Screen Recording Permission - let hasPermission = CGPreflightScreenCaptureAccess() - if !hasPermission { - CGRequestScreenCaptureAccess() - } - self.hasScreenRecordingPermission = hasPermission - - // Check Launch at Startup Status - self.isLaunchAtStartupEnabled = SMAppService.mainApp.status == .enabled - - print("โœ… Permissions check: ScreenRecording=\(self.hasScreenRecordingPermission), LaunchAtStartup=\(self.isLaunchAtStartupEnabled)") - } -} - -// MARK: - Compact Header View -struct CompactHeaderView: View { - var body: some View { - VStack(spacing: 8) { - // App logo - now using AppIcon instead of Slothhead - Group { - if let nsImage = NSImage(named: "AppIcon") { - Image(nsImage: nsImage) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 40, height: 40) - } else if let bundle = Bundle.main.url(forResource: "AppIcon", withExtension: "icns"), - let nsImage = NSImage(contentsOf: bundle) { - Image(nsImage: nsImage) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 40, height: 40) - } else { - Image(systemName: "camera.viewfinder") - .font(.system(size: 40)) - .foregroundColor(.blue) - } - } - - VStack(spacing: 2) { - Text("Welcome to ShotScreen") - .font(.title3) - .fontWeight(.bold) - .foregroundColor(.primary) - - Text("Set up your screenshot tool") - .font(.caption) - .foregroundColor(.secondary) - } - } - } -} - -// MARK: - Setup Section -struct SetupSection: View { - var hasScreenRecordingPermission: Bool - var hasCompletedScreenshotSettings: Bool - var onScreenshotSettingsCompleted: () -> Void - - var body: some View { - VStack(spacing: 16) { - // macOS Screenshot Settings with Instructions - SimpleScreenshotCardWithInstructions( - action: openMacOSScreenshotSettings, - isCompleted: hasCompletedScreenshotSettings, - onCompleted: onScreenshotSettingsCompleted - ) - - // Privacy Permissions - SinglePageCard( - icon: "shield.checkerboard", - title: "Grant Screen Recording Permission", - subtitle: "Required for screenshot capture", - actionTitle: "Open Privacy", - action: openSystemPreferences, - isCompleted: hasScreenRecordingPermission - ) - } - } - - private func openMacOSScreenshotSettings() { - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.keyboard?Shortcuts") { - NSWorkspace.shared.open(url) - } else { - print("โŒ Could not open macOS Screenshot Settings") - } - } - - private func openSystemPreferences() { - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture") { - NSWorkspace.shared.open(url) - } else { - print("โŒ Could not open System Preferences") - } - } -} - -// MARK: - Simple Screenshot Card with Instructions -struct SimpleScreenshotCardWithInstructions: View { - let action: () -> Void - var isCompleted: Bool - var onCompleted: () -> Void - @State private var showingInstructions = false - - var body: some View { - HStack(alignment: .center, spacing: 12) { - Image(systemName: "gear") - .font(.system(size: 20)) - .foregroundColor(.blue) - .frame(width: 24) - - VStack(alignment: .leading, spacing: 2) { - HStack(spacing: 6) { - Text("Disable macOS Screenshot Shortcut") - .fontWeight(.medium) - - if isCompleted { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.green) - .font(.subheadline) - } - } - - Text("Click Instructions for step-by-step guide") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button("Instructions") { - print("๐Ÿ“– Instructions button pressed") - showingInstructions = true - } - .buttonStyle(.borderedProminent) - .controlSize(.small) - } - .padding(12) - .background( - RoundedRectangle(cornerRadius: 8) - .fill(Color(NSColor.controlBackgroundColor)) - .shadow(color: .black.opacity(0.05), radius: 1, x: 0, y: 1) - ) - .sheet(isPresented: $showingInstructions) { - InstructionsPopupView(openSettingsAction: action, isCompleted: isCompleted, onCompleted: onCompleted) - } - } -} - -// MARK: - Instructions Popup View -struct InstructionsPopupView: View { - @Environment(\.presentationMode) var presentationMode - let openSettingsAction: () -> Void - var isCompleted: Bool - var onCompleted: () -> Void - @State private var imageSize: CGSize = CGSize(width: 600, height: 400) - - var body: some View { - VStack(spacing: 20) { - // Header - HStack { - Text("How to Disable macOS Screenshot Shortcut") - .font(.title2) - .fontWeight(.bold) - - Spacer() - - Button("Close") { - presentationMode.wrappedValue.dismiss() - } - .buttonStyle(.bordered) - .controlSize(.small) - } - .padding(.top, 20) - .padding(.horizontal, 20) - - // Instruction image - Group { - if let bundle = Bundle.main.url(forResource: "images/Wizard_TurnOffSceenShot", withExtension: "png"), - let nsImage = NSImage(contentsOf: bundle) { - Image(nsImage: nsImage) - .resizable() - .aspectRatio(contentMode: .fit) - .onAppear { - // Get actual image size and scale it appropriately - let maxWidth: CGFloat = 800 - let maxHeight: CGFloat = 600 - let aspectRatio = nsImage.size.width / nsImage.size.height - - if nsImage.size.width > maxWidth { - imageSize = CGSize(width: maxWidth, height: maxWidth / aspectRatio) - } else if nsImage.size.height > maxHeight { - imageSize = CGSize(width: maxHeight * aspectRatio, height: maxHeight) - } else { - imageSize = nsImage.size - } - } - .frame(width: imageSize.width, height: imageSize.height) - .background(Color.white) - .cornerRadius(8) - .shadow(radius: 2) - } else if let nsImage = NSImage(named: "Wizard_TurnOffSceenShot") { - Image(nsImage: nsImage) - .resizable() - .aspectRatio(contentMode: .fit) - .onAppear { - // Get actual image size and scale it appropriately - let maxWidth: CGFloat = 800 - let maxHeight: CGFloat = 600 - let aspectRatio = nsImage.size.width / nsImage.size.height - - if nsImage.size.width > maxWidth { - imageSize = CGSize(width: maxWidth, height: maxWidth / aspectRatio) - } else if nsImage.size.height > maxHeight { - imageSize = CGSize(width: maxHeight * aspectRatio, height: maxHeight) - } else { - imageSize = nsImage.size - } - } - .frame(width: imageSize.width, height: imageSize.height) - .background(Color.white) - .cornerRadius(8) - .shadow(radius: 2) - } else { - // Development fallback: try to load from source directory - let developmentImagePaths = [ - "ScreenShot/Sources/images/Wizard_TurnOffSceenShot.png", - "./ScreenShot/Sources/images/Wizard_TurnOffSceenShot.png", - "../ScreenShot/Sources/images/Wizard_TurnOffSceenShot.png" - ] - - if let workingImagePath = developmentImagePaths.first(where: { FileManager.default.fileExists(atPath: $0) }), - let nsImage = NSImage(contentsOfFile: workingImagePath) { - Image(nsImage: nsImage) - .resizable() - .aspectRatio(contentMode: .fit) - .onAppear { - // Get actual image size and scale it appropriately - let maxWidth: CGFloat = 800 - let maxHeight: CGFloat = 600 - let aspectRatio = nsImage.size.width / nsImage.size.height - - if nsImage.size.width > maxWidth { - imageSize = CGSize(width: maxWidth, height: maxWidth / aspectRatio) - } else if nsImage.size.height > maxHeight { - imageSize = CGSize(width: maxHeight * aspectRatio, height: maxHeight) - } else { - imageSize = nsImage.size - } - } - .frame(width: imageSize.width, height: imageSize.height) - .background(Color.white) - .cornerRadius(8) - .shadow(radius: 2) - } else { - // Final fallback if image is not found anywhere - VStack(spacing: 10) { - Image(systemName: "photo") - .font(.system(size: 50)) - .foregroundColor(.gray) - - Text("Instructions Image") - .font(.caption) - .foregroundColor(.secondary) - - Text("Image will be available in release build") - .font(.caption2) - .foregroundColor(.orange) - } - .frame(width: imageSize.width, height: imageSize.height) - .background(Color.gray.opacity(0.1)) - .cornerRadius(8) - } - } - } - .padding(.horizontal, 20) - - // Instructions text - VStack(alignment: .leading, spacing: 8) { - Text("Follow these steps:") - .font(.headline) - - Text("1. Click 'Open Settings' below to open macOS System Preferences") - Text("2. Navigate to Keyboard > Shortcuts > Screenshots") - Text("3. Uncheck 'Save picture of selected area as a file' (โŒ˜+โ‡ง+4)") - Text("4. This prevents conflicts with ShotScreen") - } - .font(.body) - .padding(.horizontal, 20) - .frame(maxWidth: .infinity, alignment: .leading) - - Spacer() - - // Bottom buttons - HStack(spacing: 12) { - Button("Open Settings") { - print("๐Ÿ”ง Opening macOS Screenshot Settings from popup...") - openSettingsAction() - // Don't dismiss popup - let user read instructions while configuring - } - .buttonStyle(.borderedProminent) - - Button("Got it!") { - print("โœ… User completed screenshot settings configuration") - onCompleted() - presentationMode.wrappedValue.dismiss() - } - .buttonStyle(.bordered) - } - .padding(.bottom, 20) - } - .frame(width: max(imageSize.width + 40, 400), height: imageSize.height + 260) - .background(Color(NSColor.windowBackgroundColor)) - } -} - -// MARK: - Configuration Section -struct ConfigurationSection: View { - var isLaunchAtStartupEnabled: Bool - - var body: some View { - // Launch at Startup - now as a button - SinglePageCard( - icon: "power", - title: "Launch at Startup", - subtitle: "Start automatically when you log in", - actionTitle: "Open Login Items", - action: openLoginItemsSettings, - isCompleted: isLaunchAtStartupEnabled - ) - } - - private func openLoginItemsSettings() { - print("๐Ÿ”ง Opening Login Items Settings...") - - // Voor macOS 13+ - gebruik eenvoudige URL - if #available(macOS 13.0, *) { - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.users") { - NSWorkspace.shared.open(url) - return - } - } - - // Voor macOS 12 en ouder - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.users") { - NSWorkspace.shared.open(url) - } else { - print("โŒ Could not open Login Items Settings") - } - } -} - -// MARK: - Single Page Card with Action -struct SinglePageCard: View { - let icon: String - let title: String - let subtitle: String - let actionTitle: String - let action: () -> Void - var isCompleted: Bool - - var body: some View { - HStack(alignment: .center, spacing: 12) { - Image(systemName: icon) - .font(.system(size: 20)) - .foregroundColor(.blue) - .frame(width: 24) - - VStack(alignment: .leading, spacing: 2) { - HStack(spacing: 6) { - Text(title) - .fontWeight(.medium) - - if isCompleted { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.green) - .font(.subheadline) - } - } - - Text(subtitle) - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button(actionTitle) { - print("๐Ÿ”ง Action button pressed: \(actionTitle)") - action() - } - .buttonStyle(.borderedProminent) - .controlSize(.small) - } - .padding(12) - .background( - RoundedRectangle(cornerRadius: 8) - .fill(Color(NSColor.controlBackgroundColor)) - .shadow(color: .black.opacity(0.05), radius: 1, x: 0, y: 1) - ) - } -} - -// MARK: - Visual Effect Background Helper -// Note: VisualEffectBackground is defined in IntegratedGalleryView.swift \ No newline at end of file diff --git a/ShotScreen/Sources/GridActionManager.swift b/ShotScreen/Sources/GridActionManager.swift deleted file mode 100644 index 72a7b36..0000000 --- a/ShotScreen/Sources/GridActionManager.swift +++ /dev/null @@ -1,443 +0,0 @@ -import AppKit -import Vision -import SwiftUI - -// MARK: - Grid Action Manager -protocol GridActionManagerDelegate: AnyObject { - func showOrUpdateStash(with imageURL: URL) - func closePreviewWithAnimation(immediate: Bool) - func ensurePreviewVisible() - func getLastImage() -> NSImage? - func getTempURL() -> URL? - func getActivePreviewWindow() -> NSWindow? - func getRenameActionHandler() -> RenameActionHandler - func flashPreviewBorder() - func disableMonitoring() - func enableMonitoring() - func gridViewManagerHideGrid(monitorForReappear: Bool) - func getGridCurrentFrame() -> NSRect? - // ๐ŸŽจ NEW: Background removal thumbnail workflow with original URL - func showBackgroundRemovalThumbnail(with image: NSImage, originalURL: URL) -} - -class GridActionManager { - weak var delegate: GridActionManagerDelegate? - - init(delegate: GridActionManagerDelegate) { - self.delegate = delegate - } - - // MARK: - Grid Action Handler - func handleGridAction(imageURL: URL, cellIndex: Int, dropPoint: NSPoint, gridWindow: NSWindow?, completion: @escaping (Bool, Bool, Bool) -> Void) { - print("โœ… GridActionManager: Handling action for cell \(cellIndex)") - - // Get the action type based on the dynamic action order - let actionOrder = SettingsManager.shared.actionOrder - guard cellIndex < actionOrder.count else { - print("โ„น๏ธ Cell index \(cellIndex) out of range for action order.") - completion(false, false, false) - return - } - - let actionType = actionOrder[cellIndex] - - switch actionType { - case .rename: - print("๐Ÿ“ Rename action triggered for \(imageURL.path)") - handleRenameAction(imageURL: imageURL, completion: completion) - case .stash: - print("โœจ Stash action triggered for \(imageURL.path)") - handleStashAction(imageURL: imageURL, completion: completion) - case .ocr: - print("๐Ÿ“‘ OCR action triggered for \(imageURL.path)") - handleOCRAction(imageURL: imageURL, dropPoint: dropPoint, gridWindow: gridWindow, completion: completion) - case .clipboard: - print("๐Ÿ“‹ Clipboard action triggered for \(imageURL.path)") - handleClipboardAction(imageURL: imageURL, dropPoint: dropPoint, gridWindow: gridWindow, completion: completion) - case .backgroundRemove: - print("๐ŸŽจ Background Remove action triggered for \(imageURL.path)") - handleBackgroundRemoveAction(imageURL: imageURL, dropPoint: dropPoint, gridWindow: gridWindow, completion: completion) - case .cancel: - print("๐Ÿšซ Cancel action triggered") - handleCancelAction(completion: completion) - case .remove: - print("๐Ÿ—‘ Remove action triggered") - handleRemoveAction(completion: completion) - } - } - - // MARK: - Individual Action Handlers - - private func handleRenameAction(imageURL: URL, completion: @escaping (Bool, Bool, Bool) -> Void) { - print("๐Ÿ” DEBUG: handleRenameAction called with URL: \(imageURL)") - print("๐Ÿ” DEBUG: File exists at URL: \(FileManager.default.fileExists(atPath: imageURL.path))") - - delegate?.getRenameActionHandler().promptAndRename(originalURL: imageURL) { responseForRename in - let successfulAction = (responseForRename != .alertThirdButtonReturn) - let saveToFolderTriggered = (responseForRename == .alertSecondButtonReturn) - print("๐Ÿ” DEBUG: Rename response: \(responseForRename), successful: \(successfulAction)") - completion(successfulAction, saveToFolderTriggered, false) - } - } - - private func handleStashAction(imageURL: URL, completion: @escaping (Bool, Bool, Bool) -> Void) { - print("โœจ Stash action triggered for \(imageURL.path)") - - // CRITICALLY IMPORTANT: Close main grid immediately after stash action - print("๐Ÿ”ถ STASH ACTION: Closing main grid and disabling proximity monitoring") - delegate?.gridViewManagerHideGrid(monitorForReappear: false) - - delegate?.showOrUpdateStash(with: imageURL) - completion(true, false, true) // Pass true for isStashAction - } - - private func handleOCRAction(imageURL: URL, dropPoint: NSPoint, gridWindow: NSWindow?, completion: @escaping (Bool, Bool, Bool) -> Void) { - delegate?.disableMonitoring() - performOcrAndCopy(from: imageURL, dropPoint: dropPoint, gridWindow: gridWindow) { success in - // Completion for performOcrAndCopy is now async after panel closes - // The actual success of OCR (text found vs. no text) is handled by the message in the panel. - // For the grid action itself, we consider it 'successful' if the process ran. - self.delegate?.enableMonitoring() - self.delegate?.gridViewManagerHideGrid(monitorForReappear: false) - completion(true, false, false) // OCR action always 'succeeds' in terms of grid interaction - } - } - - private func handleClipboardAction(imageURL: URL, dropPoint: NSPoint, gridWindow: NSWindow?, completion: @escaping (Bool, Bool, Bool) -> Void) { - delegate?.disableMonitoring() - copyImageToClipboard(from: imageURL, dropPoint: dropPoint, gridWindow: gridWindow) { - self.delegate?.enableMonitoring() - self.delegate?.gridViewManagerHideGrid(monitorForReappear: false) - completion(true, false, false) - } - } - - private func handleBackgroundRemoveAction(imageURL: URL, dropPoint: NSPoint, gridWindow: NSWindow?, completion: @escaping (Bool, Bool, Bool) -> Void) { - performBackgroundRemove(from: imageURL, dropPoint: dropPoint, gridWindow: gridWindow) - completion(true, false, false) - } - - private func handleCancelAction(completion: @escaping (Bool, Bool, Bool) -> Void) { - // Handle cancel action through delegate callback mechanism - NotificationCenter.default.post(name: .gridActionCancelRequested, object: nil) - completion(false, false, false) - } - - private func handleRemoveAction(completion: @escaping (Bool, Bool, Bool) -> Void) { - // Handle remove action through delegate callback mechanism - NotificationCenter.default.post(name: .gridActionRemoveRequested, object: nil) - completion(false, false, false) - } - - // MARK: - OCR Implementation - - private func performOcrAndCopy(from imageURL: URL, dropPoint: NSPoint, gridWindow: NSWindow?, completion: @escaping (Bool) -> Void) { - // Intel Mac compatible Vision framework OCR - guard #available(macOS 10.15, *) else { - print("โŒ OCR requires macOS 10.15 or later") - if let gridFrame = delegate?.getGridCurrentFrame() { - let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Error") - panel.show(aroundGridFrame: gridFrame, text: "OCR requires macOS 10.15+", autoCloseAfter: 1.8, onAutoCloseCompletion: { - completion(false) - }) - } else { - completion(false) - } - return - } - guard let nsImage = NSImage(contentsOf: imageURL) else { - print("โŒ OCR: Could not load image from URL: \(imageURL.path)") - if let gridFrame = delegate?.getGridCurrentFrame() { - let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Error") - panel.show(aroundGridFrame: gridFrame, text: "Image load failed for OCR", autoCloseAfter: 1.8, onAutoCloseCompletion: { - completion(false) - }) - } else { - completion(false) - } - return - } - - guard let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil) else { - print("โŒ OCR: Could not obtain CGImage") - if let gridFrame = delegate?.getGridCurrentFrame() { - let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Error") - panel.show(aroundGridFrame: gridFrame, text: "Could not get image data for OCR", autoCloseAfter: 1.8, onAutoCloseCompletion: { - completion(false) - }) - } else { - completion(false) - } - return - } - - let request = VNRecognizeTextRequest { [weak self] request, error in - guard let self = self else { completion(false); return } - - var message: String - var ocrDidFindText = false - - if let err = error { - print("โŒ OCR error: \(err.localizedDescription)") - message = "OCR error: \(err.localizedDescription)" - } else { - let observations = request.results as? [VNRecognizedTextObservation] ?? [] - let recognizedText = observations.compactMap { $0.topCandidates(1).first?.string }.joined(separator: "\n") - if recognizedText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - message = "No text found" - } else { - NSPasteboard.general.clearContents() - NSPasteboard.general.setString(recognizedText, forType: .string) - message = "Text copied to clipboard!" - ocrDidFindText = true - } - } - - DispatchQueue.main.async { - if let gridFrame = self.delegate?.getGridCurrentFrame() { - let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Processing...") - panel.show(aroundGridFrame: gridFrame, text: message, autoCloseAfter: 1.8, onAutoCloseCompletion: { - completion(ocrDidFindText) - }) - } else { - completion(ocrDidFindText) // Call completion if gridFrame is 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: Failed to perform request โ€“ \(error.localizedDescription)") - DispatchQueue.main.async { - self.showToast(message: "OCR failed", near: dropPoint, gridWindow: gridWindow) - } - } - } - } - - // MARK: - Clipboard Implementation - - private func copyImageToClipboard(from imageURL: URL, dropPoint: NSPoint, gridWindow: NSWindow?, completion: @escaping () -> Void) { - guard let nsImage = NSImage(contentsOf: imageURL) else { - print("โŒ Clipboard: Could not load image from URL: \(imageURL.path)") - // Consider showing error with FeedbackBubblePanel if gridFrame is available - if let gridFrame = delegate?.getGridCurrentFrame() { - let panel = FeedbackBubblePanel(contentRect: NSRect.zero, text: "Error") - panel.show(aroundGridFrame: gridFrame, text: "Image load failed for Clipboard", autoCloseAfter: 2.0) - panel.closeWithAnimation { completion() } - } else { - completion() - } - return - } - - NSPasteboard.general.clearContents() - NSPasteboard.general.writeObjects([nsImage]) - - if let gridFrame = delegate?.getGridCurrentFrame() { - 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() - } - } - } else { - print("โš ๏ธ Could not get grid frame to show feedback bubble for clipboard.") - completion() - } - } - - // MARK: - Background Remove Implementation - - private func performBackgroundRemove(from imageURL: URL, dropPoint: NSPoint, gridWindow: NSWindow?) { - print("๐ŸŽจ Background Remove action triggered for \(imageURL.path)") - - // ๐Ÿ”„ ENHANCED: Better error handling and recovery for image loading - var finalImageURL = imageURL - var originalImage: NSImage? = nil - - // First attempt: Direct load - originalImage = NSImage(contentsOf: finalImageURL) - - // If failed, try recovery strategies - if originalImage == nil { - print("โš ๏ธ Initial image load failed, attempting recovery...") - - // Strategy 1: Check if file exists but has load issues - if FileManager.default.fileExists(atPath: imageURL.path) { - print("๐Ÿ” File exists but NSImage can't load it, trying alternative loading...") - - // Try loading with different methods - if let imageData = try? Data(contentsOf: imageURL) { - originalImage = NSImage(data: imageData) - if originalImage != nil { - print("โœ… RECOVERED: Successfully loaded image via Data method") - } - } - } - - // Strategy 2: Check temp directory for file - if originalImage == nil { - let tempDir = FileManager.default.temporaryDirectory - let fileName = imageURL.lastPathComponent - let tempURL = tempDir.appendingPathComponent(fileName) - - if FileManager.default.fileExists(atPath: tempURL.path) { - originalImage = NSImage(contentsOf: tempURL) - if originalImage != nil { - finalImageURL = tempURL - print("โœ… RECOVERED: Found image in temp directory: \(tempURL.path)") - } - } - } - - // Strategy 3: Check if it's a cache file issue and try alternative cache locations - if originalImage == nil { - let supportDir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! - let shotScreenDir = supportDir.appendingPathComponent("ShotScreen/Thumbnails") - let cacheName = imageURL.lastPathComponent - let cacheURL = shotScreenDir.appendingPathComponent(cacheName) - - if FileManager.default.fileExists(atPath: cacheURL.path) { - originalImage = NSImage(contentsOf: cacheURL) - if originalImage != nil { - finalImageURL = cacheURL - print("โœ… RECOVERED: Found image in cache: \(cacheURL.path)") - } - } - } - } - - // Final check - guard let finalImage = originalImage else { - print("โŒ Could not load image for background removal after all recovery attempts") - print("๐Ÿ“ Attempted URLs:") - print(" - Original: \(imageURL.path)") - print(" - File exists: \(FileManager.default.fileExists(atPath: imageURL.path))") - - showToast(message: "Image load failed", near: dropPoint, gridWindow: gridWindow) - return - } - - print("โœ… Successfully loaded image for BGR: \(finalImageURL.lastPathComponent)") - print("๐ŸŽจ Background Remove thumbnail UI starting...") - - // ๐ŸŽฏ NEW: Use thumbnail-based BGR workflow via delegate with recovered URL - delegate?.showBackgroundRemovalThumbnail(with: finalImage, originalURL: finalImageURL) - } - - // MARK: - Toast Notification System - - private func showToast(message: String, near point: NSPoint, gridWindow: NSWindow?) { - let toastWidth: CGFloat = 180 - let toastHeight: CGFloat = 40 - - // Kies vaste positie links (anders rechts) naast de grid - let margin: CGFloat = 5 - var screenPoint: NSPoint - if let gw = gridWindow { - let gridFrame = gw.frame - let screenVisible = gw.screen?.visibleFrame ?? NSScreen.main!.visibleFrame - var x = gridFrame.minX - toastWidth - margin // links van grid - if x < screenVisible.minX + margin { // niet genoeg ruimte links โ†’ rechts - x = gridFrame.maxX + margin - } - let y = gridFrame.midY - toastHeight / 2 - screenPoint = NSPoint(x: x, y: y) - } else { - // Fallback: gebruik muislocatie zoals voorheen - screenPoint = NSEvent.mouseLocation - screenPoint.x -= toastWidth / 2 - screenPoint.y += 20 - } - - let toastWindow = NSWindow(contentRect: NSRect(x: screenPoint.x, y: screenPoint.y, width: toastWidth, height: toastHeight), - styleMask: [.borderless], backing: .buffered, defer: false) - toastWindow.isOpaque = false - toastWindow.backgroundColor = .clear - toastWindow.level = .floating + 5 - toastWindow.hasShadow = true - - // Opbouw content - let container = NSView(frame: NSRect(x: 0, y: 0, width: toastWidth, height: toastHeight)) - let blur = NSVisualEffectView(frame: container.bounds) - blur.blendingMode = .behindWindow - blur.material = .hudWindow - blur.state = .active - blur.alphaValue = 0.9 - blur.wantsLayer = true - blur.layer?.cornerRadius = 12 - blur.layer?.masksToBounds = true - - let label = NSTextField(labelWithString: message) - label.textColor = .white - label.alignment = .center - label.font = .systemFont(ofSize: 13, weight: .medium) - label.frame = NSRect(x: 0, y: (toastHeight - 20) / 2, width: toastWidth, height: 20) - - container.addSubview(blur) - container.addSubview(label) - toastWindow.contentView = container - toastWindow.alphaValue = 0 - toastWindow.makeKeyAndOrderFront(nil as Any?) - - NSAnimationContext.runAnimationGroup({ ctx in - ctx.duration = 0.35 - toastWindow.animator().alphaValue = 1 - }, completionHandler: { - let flyUp: CGFloat = 30 - let targetOrigin = NSPoint(x: toastWindow.frame.origin.x, y: toastWindow.frame.origin.y + flyUp) - NSAnimationContext.runAnimationGroup({ ctx in - ctx.duration = 2 - toastWindow.animator().alphaValue = 0 - toastWindow.animator().setFrameOrigin(targetOrigin) - }, completionHandler: { - toastWindow.orderOut(nil as Any?) - }) - }) - } - - // MARK: - Action Completion Handler - - func handleActionCompletion(actionCompletedSuccessfully: Bool, wasSaveToFolder: Bool, isStashAction: Bool) { - print("โ„น๏ธ Action panel completion: Success - \(actionCompletedSuccessfully), WasSaveToFolder - \(wasSaveToFolder), IsStashAction - \(isStashAction)") - - if actionCompletedSuccessfully { - if wasSaveToFolder { - // Save to folder actions handle their own preview closing via closeAfterSave setting. - print("โ„น๏ธ Save to folder action: Preview management handled by save logic.") - } else if isStashAction { - // CRITICAL: Ensure main grid stays closed after stash action - print("๐Ÿ”ถ STASH ACTION COMPLETION: Ensuring main grid stays closed - no proximity monitoring") - delegate?.gridViewManagerHideGrid(monitorForReappear: false) - - // FIXED: For stash action, preview is already properly closed in main.swift _showOrUpdateStash - // No need to close again here or check tempURL - it's already moved to stash directory - print("โ„น๏ธ Stash action: Preview management handled by stash logic in main.swift") - } else { - if delegate?.getTempURL() != nil { - print("โ„น๏ธ Other successful grid action (not save/stash): Ensuring preview is visible.") - delegate?.ensurePreviewVisible() - } - } - } else { - if delegate?.getTempURL() != nil { - print("โ„น๏ธ Action not successful or cancelled: Ensuring preview is visible.") - delegate?.ensurePreviewVisible() - } - } - } -} - -// MARK: - Notification Names -extension Notification.Name { - static let gridActionCancelRequested = Notification.Name("gridActionCancelRequested") - static let gridActionRemoveRequested = Notification.Name("gridActionRemoveRequested") -} \ No newline at end of file diff --git a/ShotScreen/Sources/GridCellView.swift b/ShotScreen/Sources/GridCellView.swift deleted file mode 100644 index 31e98ac..0000000 --- a/ShotScreen/Sources/GridCellView.swift +++ /dev/null @@ -1,194 +0,0 @@ -import AppKit - -// MARK: - Grid Cell View for Action Grid -class GridCellView: NSView { - let index: Int - private let label: NSTextField = NSTextField(labelWithString: "") - private let iconImageView: NSImageView = NSImageView() - private var iconCenterConstraint: NSLayoutConstraint! - private let originalBackgroundColor = NSColor.clear - private let highlightBackgroundColor = NSColor.clear // geen highlight - private var isHovered: Bool = false - private let iconStartOffset: CGFloat // <-- maak property - - init(frame frameRect: NSRect, index: Int, text: String) { - self.index = index - self.iconStartOffset = 30 // <-- property initialiseren - super.init(frame: frameRect) - wantsLayer = true - - // Setup theme change observer - ThemeManager.shared.observeThemeChanges { [weak self] in - DispatchQueue.main.async { - self?.updateThemeColors() - } - } - - // Voeg mouse tracking toe - let trackingArea = NSTrackingArea(rect: bounds, - options: [.mouseEnteredAndExited, .activeAlways], - owner: self, - userInfo: nil) - addTrackingArea(trackingArea) - - // Glas-achtergrond (dubbele blur) - #if false - let blur1 = NSVisualEffectView() - blur1.blendingMode = .behindWindow - blur1.material = .hudWindow - blur1.state = .active - blur1.frame = bounds - blur1.autoresizingMask = [.width, .height] - let blur2 = NSVisualEffectView() - blur2.blendingMode = .behindWindow - blur2.material = .hudWindow - blur2.state = .active - blur2.alphaValue = 0.6 - blur2.frame = bounds - blur2.autoresizingMask = [.width, .height] - addSubview(blur1, positioned: .below, relativeTo: nil) - addSubview(blur2, positioned: .below, relativeTo: nil) - #endif - - layer?.cornerRadius = 0 - layer?.masksToBounds = false - - label.stringValue = text - label.font = NSFont.systemFont(ofSize: 12, weight: .semibold) - label.textColor = ThemeManager.shared.gridCellTextColor(isHovered: false) - label.alignment = .left - label.alphaValue = 1.0 // Use theme opacity directly - label.translatesAutoresizingMaskIntoConstraints = false - addSubview(label) - - // Cirkel-achtergrond voor het icoon - let iconBackground = NSView() - iconBackground.wantsLayer = true - iconBackground.translatesAutoresizingMaskIntoConstraints = false - iconBackground.layer?.cornerRadius = 18 // voor 36ร—36 cirkel - iconBackground.layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.cgColor - addSubview(iconBackground) - - // Configuratie icon based on text content - let symbolName: String - if text.contains("Rename") { - symbolName = "pencil" - } else if text.contains("Stash") { - symbolName = "archivebox" - } else if text.contains("Text") { - symbolName = "text.viewfinder" - } else if text.contains("Clipboard") { - symbolName = "doc.on.clipboard" - } else if text.contains("Remove BG") { - symbolName = "person.and.background.dotted" - } else if text.contains("Cancel") { - symbolName = "xmark" - } else if text.contains("Remove") { - symbolName = "trash" - } else { - symbolName = "square" - } - let baseImage = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil) - let configuredImage = baseImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 18, weight: .semibold)) - iconImageView.image = configuredImage - iconImageView.contentTintColor = ThemeManager.shared.primaryTextColor - iconImageView.translatesAutoresizingMaskIntoConstraints = false - iconBackground.addSubview(iconImageView) - - // AutoLayout (start meer naar rechts) - let iconStartOffset: CGFloat = 30 // Pas dit getal aan voor meer/minder ruimte links - iconCenterConstraint = iconBackground.centerXAnchor.constraint(equalTo: centerXAnchor, constant: iconStartOffset) - - NSLayoutConstraint.activate([ - iconCenterConstraint, - iconBackground.centerYAnchor.constraint(equalTo: centerYAnchor), - iconBackground.widthAnchor.constraint(equalToConstant: 36), - iconBackground.heightAnchor.constraint(equalTo: iconBackground.widthAnchor), - - iconImageView.centerXAnchor.constraint(equalTo: iconBackground.centerXAnchor), - iconImageView.centerYAnchor.constraint(equalTo: iconBackground.centerYAnchor), - - label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12), - label.trailingAnchor.constraint(equalTo: iconBackground.leadingAnchor, constant: -8), - label.centerYAnchor.constraint(equalTo: centerYAnchor) - ]) - - // Zet initiรซle transform - layer?.anchorPoint = CGPoint(x: 0.5, y: 0.5) - layer?.transform = CATransform3DIdentity - - // Voeg subtiele gloed toe aan het label - let shadow = NSShadow() - shadow.shadowColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.4) - shadow.shadowBlurRadius = 4 - shadow.shadowOffset = NSSize(width: 0, height: -1) - label.shadow = shadow - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func mouseEntered(with event: NSEvent) { - let shift = (self.bounds.width / 2) - 18 - 2 - NSAnimationContext.runAnimationGroup { ctx in - ctx.duration = 0.15 - ctx.timingFunction = CAMediaTimingFunction(name: .easeOut) - self.iconCenterConstraint.animator().constant = shift - self.label.animator().textColor = ThemeManager.shared.gridCellTextColor(isHovered: true) - } - } - - override func mouseExited(with event: NSEvent) { - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.15 - context.timingFunction = CAMediaTimingFunction(name: .easeOut) - // Icoon terug naar startpositie - self.iconCenterConstraint.animator().constant = self.iconStartOffset - // Text color terug naar normal - self.label.animator().textColor = ThemeManager.shared.gridCellTextColor(isHovered: false) - } - } - - func setHovered(_ hovered: Bool) { - guard hovered != isHovered else { return } - isHovered = hovered - let shift = (self.bounds.width / 2) - 18 - 5 - NSAnimationContext.runAnimationGroup { ctx in - ctx.duration = 0.15 - ctx.timingFunction = CAMediaTimingFunction(name: .easeOut) - if hovered { - self.iconCenterConstraint.animator().constant = shift - self.label.animator().textColor = ThemeManager.shared.gridCellTextColor(isHovered: true) - } else { - self.iconCenterConstraint.animator().constant = self.iconStartOffset // Terug naar startpositie - self.label.animator().textColor = ThemeManager.shared.gridCellTextColor(isHovered: false) - } - } - } - - func setHighlighted(_ highlighted: Bool) { - // Do nothing (no color change) - } - - // MARK: - Theme Management - private func updateThemeColors() { - // Update text color - label.textColor = ThemeManager.shared.gridCellTextColor(isHovered: isHovered) - - // Update icon background - if let iconBackground = iconImageView.superview { - iconBackground.layer?.backgroundColor = ThemeManager.shared.gridCellIconBackground.cgColor - } - - // Update icon tint color - iconImageView.contentTintColor = ThemeManager.shared.primaryTextColor - - // Update shadow color - let shadow = NSShadow() - shadow.shadowColor = ThemeManager.shared.primaryTextColor.withAlphaComponent(0.4) - shadow.shadowBlurRadius = 4 - shadow.shadowOffset = NSSize(width: 0, height: -1) - label.shadow = shadow - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/GridComponents.swift b/ShotScreen/Sources/GridComponents.swift deleted file mode 100644 index f1b145a..0000000 --- a/ShotScreen/Sources/GridComponents.swift +++ /dev/null @@ -1,515 +0,0 @@ -import AppKit - -// MARK: - Grid Window for Action Selection -class GridWindow: NSWindow, NSDraggingDestination { - var cellViews: [GridCellView] = [] - weak var gridViewManagerDelegate: GridViewManagerDelegate? - weak var manager: GridViewManager? - private var currentlyHighlightedCell: GridCellView? - private var fadeTimer: Timer? - private let fadeStart: CGFloat = 50 // afstand in px waarbij fading start - private let fadeEnd: CGFloat = 300 // afstand waarbij minimale alpha bereikt is - private let minAlpha: CGFloat = 0 // minimale zichtbaarheid - var isInitialFadingIn: Bool = false - var isPerformingProgrammaticHide: Bool = false // NIEUWE FLAG - - init(screen: NSScreen, cellsPerRowInput: Int = 2, manager: GridViewManager, previewFrame: NSRect?) { - self.manager = manager - let settings = SettingsManager.shared - - var activeActions: [(index: Int, text: String)] = [] - - // Build actions based on settings actionOrder to respect user's preferred order - for (gridIndex, actionType) in settings.actionOrder.enumerated() { - let isEnabled: Bool - let displayText: String - - switch actionType { - case .rename: - isEnabled = settings.isRenameActionEnabled - displayText = "Rename" - case .stash: - isEnabled = settings.isStashActionEnabled - displayText = "Stash" - case .ocr: - isEnabled = settings.isOCRActionEnabled - displayText = "Text" - case .clipboard: - isEnabled = settings.isClipboardActionEnabled - displayText = "Clipboard" - case .backgroundRemove: - isEnabled = settings.isBackgroundRemoveActionEnabled - if BackgroundRemover.shared.isRMBGModelAvailable() { - displayText = "Remove BG" - } else { - displayText = "Remove BG" - } - case .cancel: - isEnabled = settings.isCancelActionEnabled - displayText = "Cancel" - case .remove: - isEnabled = settings.isRemoveActionEnabled - displayText = "Remove" - } - - if isEnabled { - // Use gridIndex as the cellIndex, which will map to actionOrder position - activeActions.append((gridIndex, displayText)) - } - } - - let numberOfActiveActions = activeActions.count - guard numberOfActiveActions > 0 else { - // Geen acties actief, maak een leeg/onzichtbaar window of handle anders - // Voor nu, een heel klein, onzichtbaar venster en return vroeg. - // Dit voorkomt een crash als numberOfCells 0 is. - super.init(contentRect: .zero, styleMask: .borderless, backing: .buffered, defer: false) - self.isOpaque = false - self.backgroundColor = .clear - self.ignoresMouseEvents = true // Belangrijk - // Roep manager.hideGrid() aan omdat er geen grid is om te tonen - DispatchQueue.main.async { manager.hideGrid() } - return - } - print("๐Ÿ”ท GridWindow init: Number of active actions = \(numberOfActiveActions)") // Print na guard - - // Verticale ActionBar: altijd 1 kolom - let cellsPerRow = 1 - let numberOfRows = Int(ceil(Double(numberOfActiveActions) / Double(cellsPerRow))) - - let spacing: CGFloat = 8 - let fixedCellHeight: CGFloat = 40.0 - let fixedCellWidth: CGFloat = 160.0 - - let calculatedGridWidth = (fixedCellWidth * CGFloat(cellsPerRow)) + (spacing * (CGFloat(cellsPerRow) + 1)) - let calculatedGridHeight = (fixedCellHeight * CGFloat(numberOfRows)) + (spacing * (CGFloat(numberOfRows) + 1)) - - var xPosition: CGFloat - var yPosition: CGFloat - - // Oude, incorrecte positionering verwijderd - if let pFrame = previewFrame { - let screenVisibleFrame = screen.visibleFrame - yPosition = pFrame.maxY + spacing - xPosition = screenVisibleFrame.maxX - calculatedGridWidth + 10 - // Bounds checking - if xPosition < screenVisibleFrame.minX + spacing { xPosition = screenVisibleFrame.minX + spacing } - if xPosition + calculatedGridWidth > screenVisibleFrame.maxX - spacing { xPosition = screenVisibleFrame.maxX - calculatedGridWidth - spacing } - if yPosition + calculatedGridHeight > screenVisibleFrame.maxY - spacing { yPosition = pFrame.minY - calculatedGridHeight - spacing } - if yPosition < screenVisibleFrame.minY + spacing { yPosition = screenVisibleFrame.minY + spacing } - } else { - let effectiveScreenFrame = screen.visibleFrame - xPosition = (effectiveScreenFrame.width - calculatedGridWidth) / 2 + effectiveScreenFrame.origin.x - yPosition = (effectiveScreenFrame.height - calculatedGridHeight) / 2 + effectiveScreenFrame.origin.y - } - - let contentRect = NSRect(x: xPosition, y: yPosition, width: calculatedGridWidth, height: calculatedGridHeight) - super.init(contentRect: contentRect, styleMask: [.borderless], backing: .buffered, defer: false) - self.level = .floating + 2 - print("๐Ÿ”ท GridWindow init: Calculated contentRect = \(contentRect)") // Print na super.init - self.isOpaque = false - self.backgroundColor = .clear - self.hasShadow = false // Was false, houd consistent - self.ignoresMouseEvents = false - self.acceptsMouseMovedEvents = true // BELANGRIJK: Voor hover events in subviews - - let containerView = NSView(frame: NSRect(origin: .zero, size: contentRect.size)) - containerView.wantsLayer = true - // Gedeelde achtergrond voor alle iconen (dubbele blur) - let barBlur1 = NSVisualEffectView() - barBlur1.blendingMode = .behindWindow - barBlur1.material = .hudWindow - barBlur1.state = .active - barBlur1.frame = containerView.bounds - barBlur1.autoresizingMask = [.width, .height] - let barBlur2 = NSVisualEffectView() - barBlur2.blendingMode = .behindWindow - barBlur2.material = .hudWindow - barBlur2.state = .active - barBlur2.alphaValue = 0.6 - barBlur2.frame = containerView.bounds - barBlur2.autoresizingMask = [.width, .height] - containerView.addSubview(barBlur1, positioned: .below, relativeTo: nil) - containerView.addSubview(barBlur2, positioned: .below, relativeTo: nil) - - containerView.layer?.cornerRadius = 12 - containerView.layer?.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] // Alle hoeken afgerond - containerView.layer?.masksToBounds = true - self.contentView = containerView - - // Maak cellen alleen voor actieve acties - for (gridIndex, action) in activeActions.enumerated() { - let col = gridIndex % cellsPerRow - let row = gridIndex / cellsPerRow - - let cellX = spacing + CGFloat(col) * (fixedCellWidth + spacing) - // Y-positie berekend van boven naar beneden voor de grid - let cellY = calculatedGridHeight - spacing - CGFloat(row + 1) * fixedCellHeight - CGFloat(row) * spacing - - let cellFrame = NSRect(x: cellX, y: cellY, width: fixedCellWidth, height: fixedCellHeight) - // Gebruik de 'index' van de actie (0 voor Rename, 1 voor Stash, etc.) voor de delegate - let cellView = GridCellView(frame: cellFrame, index: action.index, text: action.text) - containerView.addSubview(cellView) - cellViews.append(cellView) - } - - print("๐Ÿ”ท GridWindow init: Number of cellViews created = \(cellViews.count)") // Print na cell creatie - registerForDraggedTypes([.fileURL]) - - // Start timer voor dynamische transparantie - fadeTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true, block: { [weak self] _ in - self?.updateAlphaBasedOnCursor() - }) - } - - deinit { - fadeTimer?.invalidate() - } - - private func updateAlphaBasedOnCursor() { - guard !isPerformingProgrammaticHide else { return } // NIEUWE CHECK - - // NIEUW: Als monitoring uitgeschakeld is (rename actief), houd grid vol zichtbaar - if let manager = self.manager, manager.isMonitoringDisabled { - if abs(self.alphaValue - 1.0) > 0.01 { - self.alphaValue = 1.0 // Forceer volle alpha - } - return // Stop verdere alpha berekening - } - - guard !isInitialFadingIn else { return } // <-- CHECK DE FLAG - guard let screenPoint = NSEvent.mouseLocation as NSPoint? else { return } - let windowFrame = self.frame - - let distance: CGFloat - if windowFrame.contains(screenPoint) { - distance = 0 - } else { - // Bereken kortste afstand van punt naar rechthoek - let dx: CGFloat - if screenPoint.x < windowFrame.minX { dx = windowFrame.minX - screenPoint.x } - else if screenPoint.x > windowFrame.maxX { dx = screenPoint.x - windowFrame.maxX } else { dx = 0 } - - let dy: CGFloat - if screenPoint.y < windowFrame.minY { dy = windowFrame.minY - screenPoint.y } - else if screenPoint.y > windowFrame.maxY { dy = screenPoint.y - windowFrame.maxY } else { dy = 0 } - - distance = sqrt(dx*dx + dy*dy) - } - - let newAlpha: CGFloat - if distance <= fadeStart { newAlpha = 1 } - else if distance >= fadeEnd { newAlpha = minAlpha } - else { - let ratio = (distance - fadeStart) / (fadeEnd - fadeStart) - newAlpha = 1 - ratio * (1 - minAlpha) - } - - // Stel alpha direct in zonder animator voor snellere respons - if abs(self.alphaValue - newAlpha) > 0.01 { - self.alphaValue = newAlpha - } - } - - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - // MARK: - NSDraggingDestination Methods (HERSTEL DE IMPLEMENTATIES) - - func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { - // Geef aan dat we een kopieer-operatie accepteren - return .copy - } - - func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation { - let dropLocationInScreen = sender.draggingLocation - // Converteer naar coรถrdinaten binnen de content view van het grid window - guard let dropLocationInContent = self.contentView?.convert(dropLocationInScreen, from: nil) else { - // Als conversie faalt, doe niets (of reset highlight) - currentlyHighlightedCell?.setHighlighted(false) - currentlyHighlightedCell = nil - return [] - } - - var foundCell: GridCellView? = nil - // Zoek de cel onder de cursor - for cell in cellViews { - if cell.frame.contains(dropLocationInContent) { - foundCell = cell - break - } - } - - // Update highlighting alleen als de cel verandert - if currentlyHighlightedCell !== foundCell { - currentlyHighlightedCell?.setHighlighted(false) - currentlyHighlightedCell?.setHovered(false) - foundCell?.setHighlighted(true) - foundCell?.setHovered(true) - currentlyHighlightedCell = foundCell - } - - // Geef aan dat we nog steeds een kopieer-operatie accepteren - return .copy - } - - func performDragOperation(_ sender: NSDraggingInfo) -> Bool { - // Haal de bestands URL op van het gesleepte item - guard let pasteboard = sender.draggingPasteboard.propertyList(forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")) as? NSArray, - let path = pasteboard[0] as? String else { - manager?.hideGrid(monitorForReappear: true) // Verberg grid als we de data niet kunnen lezen - return false - } - let imageURL = URL(fileURLWithPath: path) - let dropLocationInContent = self.contentView?.convert(sender.draggingLocation, from: nil) ?? .zero - - // Verwijder highlight - currentlyHighlightedCell?.setHighlighted(false) - currentlyHighlightedCell = nil - - // Zoek de cel waarop gedropt is - for cell in cellViews { - if cell.frame.contains(dropLocationInContent) { - // Roep de delegate aan als deze bestaat - if let manager = self.manager, let delegate = manager.delegate { - print("โœ… GridWindow: Detected drop on cell \(cell.index). Calling delegate...") - delegate.gridViewManager(manager, didDropImage: imageURL, ontoCell: cell.index, at: dropLocationInContent) - // Het verbergen van de grid wordt nu afgehandeld door de delegate completion! - return true // Succesvolle drop - } else { - print("โŒ GridWindow: Manager or delegate is nil for drop on cell \(cell.index)!") - } - // Als manager of delegate nil is, faalt de operatie voor deze cel - return false - } - } - - // Als er niet op een cel is gedropt - manager?.hideGrid(monitorForReappear: true) - return false // Geen succesvolle drop - } - - func draggingExited(_ sender: NSDraggingInfo?) { - // Verwijder highlight en verberg grid als de cursor het window verlaat - currentlyHighlightedCell?.setHighlighted(false) - currentlyHighlightedCell?.setHovered(false) - currentlyHighlightedCell = nil - // Grid blijft zichtbaar; verbergen gebeurt elders als drag eindigt. - } - - // mouseUp override (om grid te sluiten bij klikken buiten cellen) - override func mouseUp(with event: NSEvent) { - super.mouseUp(with: event) - if !event.isARepeat { - // NIEUW: Controleer of monitoring is uitgeschakeld (rename actief) - guard let manager = self.manager, !manager.isMonitoringDisabled else { - print("๐Ÿ”ถ GridWindow: MouseUp ignored, monitoring is disabled (rename active).") - return - } - - let locationInWindow = self.contentView?.convert(event.locationInWindow, from: nil) ?? .zero - let cellUnderMouse = cellViews.first { $0.frame.contains(locationInWindow) } - if cellUnderMouse == nil { - manager.hideGrid(monitorForReappear: true) - } - } - } -} - -// MARK: - Grid View Manager -class GridViewManager { - var gridWindow: GridWindow? - weak var delegate: GridViewManagerDelegate? - - // Laatste bekende positie waarop de grid rond de preview werd getoond - private var lastPreviewFrame: NSRect? - private var reappearanceTimer: Timer? - - // Flag om monitoring uit te schakelen tijdens rename operaties (internal access) - var isMonitoringDisabled: Bool = false - - // GEFIXT: Explicit false initialization + reset in init - private var isDragSessionActive: Bool = false - - // NIEUW: Initializer om state correct te resetten - init() { - // CRITICAL: Reset all state to ensure consistent behavior between swift run and .app - self.isDragSessionActive = false - self.isMonitoringDisabled = false - self.lastPreviewFrame = nil - print("๐Ÿ”ถ GridViewManager: Initialized with clean state - isDragSessionActive: \(self.isDragSessionActive)") - } - - // MARK: - Show Grid - func showGrid(previewFrame: NSRect?) { - print("๐Ÿ”ถ MAIN DEBUG: GridViewManager: showGrid called for MAIN THUMBNAIL") - print("๐Ÿ”ถ MAIN DEBUG: This is MAIN app grid (NOT stash grid)") - print("๐Ÿ”ถ MAIN DEBUG: PreviewFrame: \(String(describing: previewFrame))") - - // Annuleer eventueel lopende timers โ€“ grid is alweer zichtbaar - reappearanceTimer?.invalidate(); reappearanceTimer = nil - - self.lastPreviewFrame = previewFrame - - if let existingWindow = gridWindow { - print("๐Ÿ”ถ MAIN DEBUG: Closing existing main grid window") - existingWindow.isPerformingProgrammaticHide = false // Reset voor het geval het vastzat - existingWindow.orderOut(nil as Any?) - self.gridWindow = nil - } - - // Bepaal het juiste scherm op basis van waar de thumbnail zich bevindt - let screen: NSScreen - if let pFrame = previewFrame { - // Zoek het scherm dat de thumbnail bevat - let thumbnailCenter = NSPoint(x: pFrame.midX, y: pFrame.midY) - if let thumbnailScreen = NSScreen.screens.first(where: { $0.frame.contains(thumbnailCenter) }) { - screen = thumbnailScreen - print("๐Ÿ”ถ MAIN DEBUG: Using thumbnail screen for main grid: \(thumbnailScreen.localizedName)") - } else { - // Fallback naar hoofdscherm als thumbnail scherm niet gevonden - screen = NSScreen.main ?? NSScreen.screens.first! - print("๐Ÿ”ถ MAIN DEBUG: Thumbnail screen not found, using fallback: \(screen.localizedName)") - } - } else { - // Geen preview frame, gebruik hoofdscherm - screen = NSScreen.main ?? NSScreen.screens.first! - print("๐Ÿ”ถ MAIN DEBUG: No preview frame, using main screen: \(screen.localizedName)") - } - - print("๐Ÿ”ถ MAIN DEBUG: Creating NEW GridWindow (main app grid, not stash)") - gridWindow = GridWindow(screen: screen, manager: self, previewFrame: previewFrame) - gridWindow?.gridViewManagerDelegate = self.delegate - gridWindow?.alphaValue = 0 - gridWindow?.makeKeyAndOrderFront(nil as Any?) // Zorg ervoor dat het key window wordt voor events - gridWindow?.orderFrontRegardless() - gridWindow?.isInitialFadingIn = true // <-- ZET FLAG VOOR ANIMATIE - - print("๐Ÿ”ถ MAIN DEBUG: Animating main grid appearance") - NSAnimationContext.runAnimationGroup { ctx in - ctx.duration = 1.0 // AANGEPAST van 0.2 naar 1.0 - self.gridWindow?.animator().alphaValue = 1 - } completionHandler: { // <-- COMPLETION HANDLER TOEGEVOEGD - self.gridWindow?.isInitialFadingIn = false // <-- RESET FLAG NA ANIMATIE - } - } - - // MARK: - Hide Grid - func hideGrid(monitorForReappear: Bool = false) { - print("๐Ÿ”ถ MAIN DEBUG: GridViewManager: hideGrid called for MAIN THUMBNAIL") - print("๐Ÿ”ถ MAIN DEBUG: monitorForReappear = \(monitorForReappear)") - guard let window = gridWindow else { - print("๐Ÿ”ถ MAIN DEBUG: No main grid window to hide") - return - } - - window.isPerformingProgrammaticHide = true // ZET FLAG VOOR ANIMATIE - - // Stop een bestaande timer zodat we niet meerdere tegelijk hebben - if !monitorForReappear { - reappearanceTimer?.invalidate(); reappearanceTimer = nil - } - - print("๐Ÿ”ถ MAIN DEBUG: Hiding main grid window") - NSAnimationContext.runAnimationGroup({ ctx in - ctx.duration = 0.8 // Teruggezet naar 0.8s - window.animator().alphaValue = 0 - }, completionHandler: { [weak self] in - guard let self = self else { return } - print("๐Ÿ”ถ MAIN DEBUG: Main grid hide animation complete") - window.orderOut(nil as Any?) - if self.gridWindow === window { - self.gridWindow = nil - } - // Reset flag na animatie en orderOut, maar VOOR potentiรซle startReappearanceMonitor - // Echter, window referentie is nu mogelijk nil als self.gridWindow === window was. - // Het is veiliger om de flag te resetten via een referentie die nog geldig is, of de logica herzien. - // Voor nu: als window nog bestaat (niet de self.gridWindow was die nil werd), reset het. - // Maar de window instance zelf wordt niet direct nil. We kunnen het nog steeds gebruiken. - window.isPerformingProgrammaticHide = false - - // Start monitor na het verbergen ALLEEN als monitoring niet uitgeschakeld is - if monitorForReappear && !self.isMonitoringDisabled { - self.startReappearanceMonitor() - } - }) - } - - // NIEUW: Methodes om monitoring te controleren - func disableMonitoring() { - print("๐Ÿ”ถ GridViewManager: Monitoring disabled (e.g., during rename)") - isMonitoringDisabled = true - // NIEUW: Stop de reappearance timer volledig - reappearanceTimer?.invalidate() - reappearanceTimer = nil - print("๐Ÿ”ถ GridViewManager: Reappearance timer stopped and invalidated") - } - - func enableMonitoring() { - print("๐Ÿ”ถ GridViewManager: Monitoring enabled") - isMonitoringDisabled = false - // GEEN automatische herstart van timer hier - alleen als grid opnieuw wordt verborgen - } - - // NIEUW: Start drag session - schakelt proximity monitoring in - func startDragSession() { - print("๐Ÿ”ถ GridViewManager: Drag session started - enabling proximity monitoring") - isDragSessionActive = true - } - - // NIEUW: Stop drag session - schakelt proximity monitoring uit - func stopDragSession() { - print("๐Ÿ”ถ GridViewManager: Drag session ended - disabling proximity monitoring") - isDragSessionActive = false - // Stop proximity timer als er geen drag actief is - reappearanceTimer?.invalidate() - reappearanceTimer = nil - } - - // MARK: - Monitoring Logic - private func startReappearanceMonitor() { - // GEFIXT: Check of monitoring uitgeschakeld is EN of er een actieve drag sessie is - // CRITICAL: Only start proximity monitoring during active drag sessions - guard !isMonitoringDisabled && isDragSessionActive else { - print("๐Ÿ”ถ GridViewManager: Skipping reappearance monitor - monitoring disabled: \(isMonitoringDisabled), drag active: \(isDragSessionActive)") - print("๐Ÿ”ถ GridViewManager: This prevents grid from triggering without actual drag") - return - } - - // Safety: invalideer vorige timer - reappearanceTimer?.invalidate() - - guard let targetFrame = lastPreviewFrame else { - print("๐Ÿ”ถ GridViewManager: No previewFrame โ€“ skipping reappearance monitor.") - return - } - - print("๐Ÿ”ถ GridViewManager: Starting proximity monitoring (only during active drag)") - reappearanceTimer = Timer.scheduledTimer(withTimeInterval: 0.12, repeats: true) { [weak self] _ in - self?.evaluateMouseProximity(to: targetFrame) - } - RunLoop.main.add(reappearanceTimer!, forMode: .common) - } - - private func evaluateMouseProximity(to frame: NSRect) { - // GEFIXT: Check of monitoring uitgeschakeld is EN of er een actieve drag sessie is - // CRITICAL: Only evaluate proximity during active drag sessions - guard !isMonitoringDisabled && isDragSessionActive else { - print("๐Ÿ”ถ GridViewManager: Skipping proximity evaluation - monitoring disabled: \(isMonitoringDisabled), drag active: \(isDragSessionActive)") - // NIEUW: Stop timer when drag session is not active - reappearanceTimer?.invalidate() - reappearanceTimer = nil - return - } - - // Bereken een vergrote zone (200 px marge) rondom de preview - let expansion: CGFloat = 200 - let enlarged = frame.insetBy(dx: -expansion, dy: -expansion) - - let currentLoc = NSEvent.mouseLocation - if enlarged.contains(currentLoc) { - // Cursor is weer in de buurt โ€“ toon grid opnieuw - print("๐Ÿ”ถ GridViewManager: Mouse near preview โ€“ showing grid again.") - self.showGrid(previewFrame: self.lastPreviewFrame) - } - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/IntegratedGalleryView.swift b/ShotScreen/Sources/IntegratedGalleryView.swift deleted file mode 100644 index 1466002..0000000 --- a/ShotScreen/Sources/IntegratedGalleryView.swift +++ /dev/null @@ -1,2666 +0,0 @@ -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 - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/LicenseEntryView.swift b/ShotScreen/Sources/LicenseEntryView.swift deleted file mode 100644 index dd41438..0000000 --- a/ShotScreen/Sources/LicenseEntryView.swift +++ /dev/null @@ -1,473 +0,0 @@ -import SwiftUI -import AppKit - -struct LicenseEntryView: View { - @ObservedObject private var licenseManager = LicenseManager.shared - @Environment(\.presentationMode) var presentationMode - - @State private var licenseKey = "" - @State private var userName = "" - @State private var isVerifying = false - @State private var errorMessage = "" - @State private var showError = false - - // Add closure for window dismissal - var onDismiss: (() -> Void)? - - init(onDismiss: (() -> Void)? = nil) { - self.onDismiss = onDismiss - } - - var body: some View { - VStack(spacing: 0) { - ScrollView { - VStack(spacing: 24) { - // Add minimal top padding since traffic lights are hidden - Spacer() - .frame(height: 20) - - VStack(spacing: 24) { - // Header Section - similar to SettingsTabView - VStack(spacing: 16) { - // Replace key icon with ShotScreen icon at native 200x200 resolution - Group { - if let shotScreenIcon = NSImage(named: "ShotScreenIcon_200x200") { - Image(nsImage: shotScreenIcon) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 200, height: 200) - .cornerRadius(20) - } else if let bundle = Bundle.main.url(forResource: "images/ShotScreenIcon_200x200", withExtension: "png"), - let nsImage = NSImage(contentsOf: bundle) { - Image(nsImage: nsImage) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 200, height: 200) - .cornerRadius(20) - } else { - // Fallback to system icon if ShotScreenIcon_200x200 not found - Image(systemName: "key.fill") - .font(.system(size: 48)) - .foregroundColor(.blue) - } - } - - VStack(spacing: 8) { - Text(headerTitle) - .font(.largeTitle) - .fontWeight(.bold) - .foregroundColor(.primary) - .multilineTextAlignment(.center) - - Text(headerSubtitle) - .font(.subheadline) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - } - } - .padding(.top, 10) - .padding(.bottom, 10) - - // Trial Status Section - switch licenseManager.licenseStatus { - case .trial(let daysLeft): - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 8) { - Text("Trial Status") - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.primary) - Divider() - } - - HStack { - Image(systemName: "clock.fill") - .foregroundColor(.orange) - Text("\(daysLeft) days remaining in your free trial") - .foregroundColor(.secondary) - } - - Text("Already purchased? Enter your license key below to activate the full version.") - .font(.caption) - .foregroundColor(.secondary) - } - .padding(20) - .background(Color.primary.opacity(0.03)) - .cornerRadius(12) - - case .expired: - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 8) { - Text("Trial Expired") - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.red) - Divider() - } - - HStack { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(.red) - Text("Your 7-day trial has expired") - .foregroundColor(.secondary) - } - - Text("Purchase a license or enter your license key to continue using ShotScreen.") - .font(.caption) - .foregroundColor(.secondary) - } - .padding(20) - .background(Color.red.opacity(0.05)) - .cornerRadius(12) - - default: - EmptyView() - } - - // License Entry Form Section - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 8) { - Text("License Information") - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.primary) - Divider() - } - - VStack(spacing: 16) { - VStack(alignment: .leading, spacing: 8) { - Text("Your Name") - .font(.headline) - .foregroundColor(.primary) - TextField("Enter your full name", text: $userName) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .disabled(isVerifying) - } - - VStack(alignment: .leading, spacing: 8) { - Text("License Key") - .font(.headline) - .foregroundColor(.primary) - TextField("XXXX-XXXX-XXXX-XXXX", text: $licenseKey) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .font(.system(.body, design: .monospaced)) - .disabled(isVerifying) - } - - if showError { - HStack { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(.red) - Text(errorMessage) - .foregroundColor(.red) - .font(.caption) - } - .padding(.horizontal, 12) - .padding(.vertical, 8) - .background(Color.red.opacity(0.1)) - .cornerRadius(8) - } - } - } - .padding(20) - .background(Color.primary.opacity(0.03)) - .cornerRadius(12) - - Spacer() - } - .padding() - } - } - - Divider() - - // Bottom Button Section - matching SettingsTabView style - HStack { - Button(action: activateLicense) { - HStack { - if isVerifying { - ProgressView() - .scaleEffect(0.7) - } - Text(isVerifying ? "Verifying..." : "Activate License") - } - } - .buttonStyle(.borderedProminent) - .disabled(!canActivate || isVerifying) - .keyboardShortcut(.defaultAction) // Return key = Activate - - Spacer() - - Button("Buy License") { - openPurchaseURL() - } - .buttonStyle(.bordered) - - Button("Cancel") { - dismissWindow() - } - .buttonStyle(.bordered) - } - .padding() - } - .frame(minWidth: 600, idealWidth: 650, minHeight: 700, idealHeight: 750) - .background(Color.clear) - } - - private var canActivate: Bool { - !licenseKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && - !userName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - } - - private var headerTitle: String { - switch licenseManager.licenseStatus { - case .trial(_): - return "Activate ShotScreen" - case .expired: - return "Unlock ShotScreen" - default: - return "Activate ShotScreen" - } - } - - private var headerSubtitle: String { - switch licenseManager.licenseStatus { - case .trial(_): - return "Already have a license? Enter your license key below." - case .expired: - return "Your free trial has ended. Enter your license key to continue using ShotScreen." - default: - return "Enter your license key to unlock the full version" - } - } - - private func activateLicense() { - guard canActivate else { return } - - isVerifying = true - showError = false - errorMessage = "" - - Task { - let success = await licenseManager.enterLicenseKey( - licenseKey.trimmingCharacters(in: .whitespacesAndNewlines), - userName: userName.trimmingCharacters(in: .whitespacesAndNewlines) - ) - - await MainActor.run { - isVerifying = false - - if success { - dismissWindow() - } else { - showError = true - errorMessage = "Invalid license key or verification failed. Please check your license key and internet connection." - } - } - } - } - - private func openPurchaseURL() { - // ShotScreen Gumroad license product - if let url = URL(string: "https://roodenrijs.gumroad.com/l/uxexr") { - NSWorkspace.shared.open(url) - } - } - - private func dismissWindow() { - onDismiss?() - presentationMode.wrappedValue.dismiss() - } -} - -// MARK: - License Entry Window -class LicenseEntryWindow: NSWindow { - private var visualEffectViewContainer: NSView? - - override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { - super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) - setupWindow() - } - - convenience init() { - self.init( - contentRect: NSRect(x: 0, y: 0, width: 650, height: 750), - styleMask: [.titled, .closable, .resizable, .fullSizeContentView], - backing: .buffered, - defer: false - ) - } - - private func setupWindow() { - title = "ShotScreen License" - isReleasedWhenClosed = false - center() - - // Ensure the window appears in front and can be brought to front - level = .normal - collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] - - // Apply same blur style as SettingsWindow - isOpaque = false - backgroundColor = .clear - titlebarAppearsTransparent = true - titleVisibility = .hidden - isMovableByWindowBackground = true - - // Hide traffic lights (red, yellow, green buttons) to prevent content overlap - standardWindowButton(.closeButton)?.isHidden = true - standardWindowButton(.miniaturizeButton)?.isHidden = true - standardWindowButton(.zoomButton)?.isHidden = true - - // Create content view with dismissal callback - let licenseView = LicenseEntryView(onDismiss: { [weak self] in - self?.close() - }) - let hostingView = NSHostingView(rootView: licenseView) - - // Apply same visual effect as SettingsWindow - let visualEffectView = NSVisualEffectView() - visualEffectView.blendingMode = .behindWindow - visualEffectView.material = .hudWindow // Same strong system blur - visualEffectView.state = .active - visualEffectView.alphaValue = 1.0 - visualEffectView.autoresizingMask = [.width, .height] - - // Extra blur layer for enhanced effect - let extraBlurView = NSVisualEffectView() - extraBlurView.blendingMode = .behindWindow - extraBlurView.material = .hudWindow - extraBlurView.state = .active - extraBlurView.alphaValue = 0.6 // Half transparent for optical blur enhancement - extraBlurView.autoresizingMask = [.width, .height] - - let newRootContentView = NSView(frame: contentRect(forFrameRect: frame)) - - // Layer order: strongest blur at bottom, half blur above, SwiftUI content on top - visualEffectView.frame = newRootContentView.bounds - extraBlurView.frame = newRootContentView.bounds - - newRootContentView.addSubview(visualEffectView) // Layer 0 - newRootContentView.addSubview(extraBlurView) // Layer 1 (enhancement) - newRootContentView.addSubview(hostingView) // Layer 2 (UI) - - contentView = newRootContentView - visualEffectViewContainer = newRootContentView - - hostingView.wantsLayer = true - hostingView.layer?.backgroundColor = NSColor.clear.cgColor - - hostingView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - hostingView.topAnchor.constraint(equalTo: newRootContentView.topAnchor), - hostingView.bottomAnchor.constraint(equalTo: newRootContentView.bottomAnchor), - hostingView.leadingAnchor.constraint(equalTo: newRootContentView.leadingAnchor), - hostingView.trailingAnchor.constraint(equalTo: newRootContentView.trailingAnchor) - ]) - - // Force a layout pass after setting up the hierarchy - contentView?.layoutSubtreeIfNeeded() - - // Window appearance - delegate = self - - // Better window management - bring to front - orderFrontRegardless() - makeKeyAndOrderFront(nil) - } - - deinit { - print("๐Ÿ—‘๏ธ LicenseEntryWindow deinitialized safely") - } -} - -// MARK: - Window Delegate -extension LicenseEntryWindow: NSWindowDelegate { - func windowShouldClose(_ sender: NSWindow) -> Bool { - // Allow closing - return true - } - - func windowWillClose(_ notification: Notification) { - // Clean up when window is about to close - contentView = nil - delegate = nil - print("๐ŸชŸ License window closing gracefully") - } -} - -// MARK: - Trial Expired View -struct TrialExpiredView: View { - @ObservedObject private var licenseManager = LicenseManager.shared - - var body: some View { - VStack(spacing: 24) { - // Replace with ShotScreen icon using 200x200 source at smaller display size - Group { - if let shotScreenIcon = NSImage(named: "ShotScreenIcon_200x200") { - Image(nsImage: shotScreenIcon) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 100, height: 100) - .cornerRadius(16) - } else if let bundle = Bundle.main.url(forResource: "images/ShotScreenIcon_200x200", withExtension: "png"), - let nsImage = NSImage(contentsOf: bundle) { - Image(nsImage: nsImage) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 100, height: 100) - .cornerRadius(16) - } else { - // Fallback icon - Image(systemName: "exclamationmark.triangle.fill") - .font(.system(size: 64)) - .foregroundColor(.orange) - } - } - - // Title - Text("Trial Expired") - .font(.largeTitle) - .fontWeight(.bold) - - // Message - Text("Your 7-day trial of ShotScreen has expired.\nPlease purchase a license to continue using the app.") - .font(.body) - .multilineTextAlignment(.center) - .foregroundColor(.secondary) - - // Buttons - matching new style - VStack(spacing: 12) { - Button("Purchase License") { - openPurchaseURL() - } - .buttonStyle(.borderedProminent) - - Button("Enter License Key") { - licenseManager.showLicenseEntryDialog() - } - .buttonStyle(.bordered) - - Button("Quit ShotScreen") { - NSApplication.shared.terminate(nil) - } - .buttonStyle(.bordered) - .foregroundColor(.red) - } - } - .padding(40) - .frame(width: 400, height: 350) - .background(Color(NSColor.windowBackgroundColor)) - } - - private func openPurchaseURL() { - // ShotScreen Gumroad license product - if let url = URL(string: "https://roodenrijs.gumroad.com/l/uxexr") { - NSWorkspace.shared.open(url) - } - } -} - -#Preview { - LicenseEntryView() -} \ No newline at end of file diff --git a/ShotScreen/Sources/LicenseManager.swift b/ShotScreen/Sources/LicenseManager.swift deleted file mode 100644 index a137ef9..0000000 --- a/ShotScreen/Sources/LicenseManager.swift +++ /dev/null @@ -1,605 +0,0 @@ -import Foundation -import Combine -import AppKit -import Security - -// MARK: - Keychain Helper -class KeychainHelper { - static let shared = KeychainHelper() - private let serviceName = "com.shotscreen.app" - - private init() {} - - // MARK: - Generic Keychain Operations - func save(_ data: Data, forKey key: String) -> Bool { - // Delete any existing item first - delete(forKey: key) - - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: serviceName, - kSecAttrAccount as String: key, - kSecValueData as String: data, - kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly - ] - - let status = SecItemAdd(query as CFDictionary, nil) - return status == errSecSuccess - } - - func load(forKey key: String) -> Data? { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: serviceName, - kSecAttrAccount as String: key, - kSecMatchLimit as String: kSecMatchLimitOne, - kSecReturnData as String: true - ] - - var result: AnyObject? - let status = SecItemCopyMatching(query as CFDictionary, &result) - - guard status == errSecSuccess else { - return nil - } - - return result as? Data - } - - func delete(forKey key: String) -> Bool { - let query: [String: Any] = [ - kSecClass as String: kSecClassGenericPassword, - kSecAttrService as String: serviceName, - kSecAttrAccount as String: key - ] - - let status = SecItemDelete(query as CFDictionary) - return status == errSecSuccess || status == errSecItemNotFound - } - - // MARK: - String Convenience Methods - func saveString(_ string: String, forKey key: String) -> Bool { - guard let data = string.data(using: .utf8) else { return false } - return save(data, forKey: key) - } - - func loadString(forKey key: String) -> String? { - guard let data = load(forKey: key) else { return nil } - return String(data: data, encoding: .utf8) - } - - // MARK: - Date Convenience Methods - func saveDate(_ date: Date, forKey key: String) -> Bool { - let data = date.timeIntervalSince1970.description.data(using: .utf8)! - return save(data, forKey: key) - } - - func loadDate(forKey key: String) -> Date? { - guard let string = loadString(forKey: key), - let timeInterval = Double(string) else { return nil } - return Date(timeIntervalSince1970: timeInterval) - } - - // MARK: - Bool Convenience Methods - func saveBool(_ bool: Bool, forKey key: String) -> Bool { - let data = bool.description.data(using: .utf8)! - return save(data, forKey: key) - } - - func loadBool(forKey key: String) -> Bool? { - guard let string = loadString(forKey: key) else { return nil } - return Bool(string) - } - - // MARK: - Migration from UserDefaults - func migrateFromUserDefaults() { - print("๐Ÿ” KEYCHAIN: Migrating sensitive data from UserDefaults to Keychain...") - - let keysToMigrate = [ - "ShotScreen_LicenseKey", - "ShotScreen_UserName", - "ShotScreen_UserEmail", - "ShotScreen_TrialStartDate", - "ShotScreen_LastVerification", - "ShotScreen_IsTestLicense" - ] - - var migratedCount = 0 - - for key in keysToMigrate { - // Check if already exists in Keychain - if load(forKey: key) != nil { - continue // Already migrated - } - - // Try to get from UserDefaults - if let string = UserDefaults.standard.string(forKey: key) { - if saveString(string, forKey: key) { - UserDefaults.standard.removeObject(forKey: key) - migratedCount += 1 - print("โœ… KEYCHAIN: Migrated \(key)") - } - } else if let date = UserDefaults.standard.object(forKey: key) as? Date { - if saveDate(date, forKey: key) { - UserDefaults.standard.removeObject(forKey: key) - migratedCount += 1 - print("โœ… KEYCHAIN: Migrated \(key)") - } - } else if UserDefaults.standard.object(forKey: key) != nil { - let bool = UserDefaults.standard.bool(forKey: key) - if saveBool(bool, forKey: key) { - UserDefaults.standard.removeObject(forKey: key) - migratedCount += 1 - print("โœ… KEYCHAIN: Migrated \(key)") - } - } - } - - if migratedCount > 0 { - print("๐Ÿ” KEYCHAIN: Successfully migrated \(migratedCount) items to secure storage") - } - } -} - -// MARK: - License Models -struct GumroadVerifyResponse: Codable { - let success: Bool - let message: String? - let uses: Int? - let purchase: GumroadPurchase? -} - -struct GumroadPurchase: Codable { - let seller_id: String? - let product_id: String? - let product_name: String? - let email: String? - let price: Int? - let currency: String? - let sale_timestamp: String? - let refunded: Bool? - let disputed: Bool? - let chargebacked: Bool? - let license_key: String? - let id: String? - let test: Bool? // Test purchase detection -} - -enum LicenseStatus { - case checking - case trial(daysLeft: Int) - case licensed(userName: String, email: String) - case testLicense(userName: String, email: String) // Test purchase status - case expired - case invalid -} - -// MARK: - License Manager -class LicenseManager: ObservableObject { - static let shared = LicenseManager() - - @Published var licenseStatus: LicenseStatus = .checking - @Published var isLicenseValid: Bool = false - @Published var showLicenseEntry: Bool = false - - // Window management - strong reference to prevent deallocation - private var licenseEntryWindow: LicenseEntryWindow? - private var windowCloseObserver: NSObjectProtocol? - - // Configuration - jouw Gumroad product info - private let productID = "338g8-ukf3N_mZ-c-XddFw==" // ShotScreen Product ID van Gumroad - private let gumroadAPIURL = "https://api.gumroad.com/v2/licenses/verify" - - // Secure Keychain keys (instead of UserDefaults) - private let licenseKeyKey = "ShotScreen_LicenseKey" - private let trialStartDateKey = "ShotScreen_TrialStartDate" - private let userNameKey = "ShotScreen_UserName" - private let userEmailKey = "ShotScreen_UserEmail" - private let lastVerificationKey = "ShotScreen_LastVerification" - private let isTestLicenseKey = "ShotScreen_IsTestLicense" - - private let trialDuration: TimeInterval = 7 * 24 * 60 * 60 // 7 dagen - private let verificationInterval: TimeInterval = 24 * 60 * 60 // 1 dag - - private init() { - // Check if we're in development mode (to avoid constant Keychain prompts) - if !isInDevelopmentMode() { - // Migrate existing data from UserDefaults to Keychain - KeychainHelper.shared.migrateFromUserDefaults() - } - checkLicenseStatus() - } - - // MARK: - Development Mode Helper - private func isInDevelopmentMode() -> Bool { - return ProcessInfo.processInfo.environment["SHOTSCREEN_DEV_MODE"] == "true" - } - - // MARK: - Development-Safe Storage Helpers - private func saveString(_ value: String, forKey key: String) { - if isInDevelopmentMode() { - UserDefaults.standard.set(value, forKey: key) - print("๐Ÿ”ง DEV MODE: Saved \(key) to UserDefaults") - } else { - _ = KeychainHelper.shared.saveString(value, forKey: key) - } - } - - private func loadString(forKey key: String) -> String? { - if isInDevelopmentMode() { - return UserDefaults.standard.string(forKey: key) - } else { - return KeychainHelper.shared.loadString(forKey: key) - } - } - - private func saveDate(_ value: Date, forKey key: String) { - if isInDevelopmentMode() { - UserDefaults.standard.set(value, forKey: key) - print("๐Ÿ”ง DEV MODE: Saved \(key) to UserDefaults") - } else { - _ = KeychainHelper.shared.saveDate(value, forKey: key) - } - } - - private func loadDate(forKey key: String) -> Date? { - if isInDevelopmentMode() { - return UserDefaults.standard.object(forKey: key) as? Date - } else { - return KeychainHelper.shared.loadDate(forKey: key) - } - } - - private func saveBool(_ value: Bool, forKey key: String) { - if isInDevelopmentMode() { - UserDefaults.standard.set(value, forKey: key) - print("๐Ÿ”ง DEV MODE: Saved \(key) to UserDefaults") - } else { - _ = KeychainHelper.shared.saveBool(value, forKey: key) - } - } - - private func loadBool(forKey key: String) -> Bool? { - if isInDevelopmentMode() { - return UserDefaults.standard.object(forKey: key) != nil ? UserDefaults.standard.bool(forKey: key) : nil - } else { - return KeychainHelper.shared.loadBool(forKey: key) - } - } - - private func deleteKey(_ key: String) { - if isInDevelopmentMode() { - UserDefaults.standard.removeObject(forKey: key) - print("๐Ÿ”ง DEV MODE: Deleted \(key) from UserDefaults") - } else { - _ = KeychainHelper.shared.delete(forKey: key) - } - } - - deinit { - cleanupLicenseWindow() - print("๐Ÿ—‘๏ธ LicenseManager deinitialized") - } - - // MARK: - Public Methods - - func checkLicenseStatus() { - print("๐Ÿ” LICENSE: Checking license status...") - - // Check if we have a stored license key (with dev mode support) - let licenseKey = loadString(forKey: licenseKeyKey) - if isInDevelopmentMode() { - print("๐Ÿ”ง DEV MODE: Using UserDefaults for license storage") - } - - if let licenseKey = licenseKey, !licenseKey.isEmpty { - print("๐Ÿ” LICENSE: Found stored license key, verifying...") - Task { - await verifyStoredLicense(licenseKey: licenseKey) - } - return - } - - // No license found - check trial status - checkTrialStatus() - } - - func enterLicenseKey(_ licenseKey: String, userName: String) async -> Bool { - print("๐Ÿ” LICENSE: Attempting to verify license key for user: \(userName)") - - let success = await verifyLicenseWithGumroad(licenseKey: licenseKey, userName: userName) - - if success { - // Store license info securely (Keychain or UserDefaults in dev mode) - saveString(licenseKey, forKey: licenseKeyKey) - saveString(userName, forKey: userNameKey) - saveDate(Date(), forKey: lastVerificationKey) - - await MainActor.run { - let isTestLicense = self.loadBool(forKey: self.isTestLicenseKey) ?? false - let email = self.getStoredEmail() - - if isTestLicense { - self.licenseStatus = .testLicense(userName: userName, email: email) - print("๐Ÿงช LICENSE: Test license activated for \(userName)") - } else { - self.licenseStatus = .licensed(userName: userName, email: email) - print("โœ… LICENSE: Production license activated for \(userName)") - } - - self.isLicenseValid = true - self.showLicenseEntry = false - } - - print("โœ… LICENSE: License activated successfully for \(userName)") - } else { - print("โŒ LICENSE: License verification failed") - } - - return success - } - - func startTrial() { - // In freemium model, trial starts automatically on first launch - // This method should not restart an expired trial - if loadDate(forKey: trialStartDateKey) != nil { - print("โš ๏ธ LICENSE: Trial already started - cannot restart trial in freemium model") - return - } - - print("๐ŸŽ‰ LICENSE: Starting trial period...") - saveDate(Date(), forKey: trialStartDateKey) - checkTrialStatus() - } - - func getTrialDaysLeft() -> Int { - guard let trialStartDate = loadDate(forKey: trialStartDateKey) else { - return 7 // No trial started yet - } - - let elapsed = Date().timeIntervalSince(trialStartDate) - let daysElapsed = Int(elapsed / (24 * 60 * 60)) - return max(0, 7 - daysElapsed) - } - - func showLicenseEntryDialog() { - print("๐Ÿ” LICENSE: Showing license entry dialog...") - - // Close existing window if any - closeLicenseEntryWindow() - - // Create and show new window with proper reference management - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - self.licenseEntryWindow = LicenseEntryWindow() - - // Set up completion handler for when window is closed - if let window = self.licenseEntryWindow { - // Add notification observer for window closing - self.windowCloseObserver = NotificationCenter.default.addObserver( - forName: NSWindow.willCloseNotification, - object: window, - queue: .main - ) { [weak self] _ in - print("๐ŸชŸ License window will close - cleaning up reference") - self?.cleanupLicenseWindow() - } - - window.makeKeyAndOrderFront(nil) - print("๐Ÿ” LICENSE: License entry window created and shown") - } - } - } - - func closeLicenseEntryWindow() { - if let window = licenseEntryWindow { - print("๐ŸชŸ Closing existing license entry window") - window.close() - } - cleanupLicenseWindow() - } - - private func cleanupLicenseWindow() { - // Remove notification observer - if let observer = windowCloseObserver { - NotificationCenter.default.removeObserver(observer) - windowCloseObserver = nil - print("๐Ÿ—‘๏ธ Window close observer removed") - } - - // Clear window reference - licenseEntryWindow = nil - print("๐Ÿ—‘๏ธ License window reference cleared") - } - - func isTrialExpired() -> Bool { - guard let trialStartDate = loadDate(forKey: trialStartDateKey) else { - return false // No trial started - } - - let elapsed = Date().timeIntervalSince(trialStartDate) - return elapsed > trialDuration - } - - func hasValidLicense() -> Bool { - return isLicenseValid - } - - // MARK: - Private Methods - - private func checkTrialStatus() { - // Check if trial has already been started - if let trialStartDate = loadDate(forKey: trialStartDateKey) { - let elapsed = Date().timeIntervalSince(trialStartDate) - - if elapsed > trialDuration { - print("โŒ LICENSE: Trial period expired (\(Int(elapsed / (24 * 60 * 60))) days ago)") - DispatchQueue.main.async { - self.licenseStatus = .expired - self.isLicenseValid = false - } - } else { - let daysLeft = max(0, 7 - Int(elapsed / (24 * 60 * 60))) - print("โฐ LICENSE: Trial active, \(daysLeft) days remaining") - DispatchQueue.main.async { - self.licenseStatus = .trial(daysLeft: daysLeft) - self.isLicenseValid = true - } - } - } else { - // First launch - automatically start trial - print("๐ŸŽ‰ LICENSE: First launch detected - automatically starting 7-day trial") - let now = Date() - saveDate(now, forKey: trialStartDateKey) - - DispatchQueue.main.async { - self.licenseStatus = .trial(daysLeft: 7) - self.isLicenseValid = true - } - } - } - - private func verifyStoredLicense(licenseKey: String) async { - // Check if we need to reverify (not more than once per day) - if let lastVerification = loadDate(forKey: lastVerificationKey) { - let timeSinceLastCheck = Date().timeIntervalSince(lastVerification) - if timeSinceLastCheck < verificationInterval { - print("๐Ÿ” LICENSE: Using cached verification") - await MainActor.run { - let userName = self.getStoredUserName() - let email = self.getStoredEmail() - let isTestLicense = self.loadBool(forKey: self.isTestLicenseKey) ?? false - - if isTestLicense { - self.licenseStatus = .testLicense(userName: userName, email: email) - print("๐Ÿงช LICENSE: Using cached test license") - } else { - self.licenseStatus = .licensed(userName: userName, email: email) - print("๐Ÿ” LICENSE: Using cached production license") - } - - self.isLicenseValid = true - } - return - } - } - - // Re-verify with Gumroad - let userName = getStoredUserName() - let success = await verifyLicenseWithGumroad(licenseKey: licenseKey, userName: userName) - - await MainActor.run { - if success { - let isTestLicense = self.loadBool(forKey: self.isTestLicenseKey) ?? false - let email = self.getStoredEmail() - - if isTestLicense { - self.licenseStatus = .testLicense(userName: userName, email: email) - print("๐Ÿงช LICENSE: Test license verified on startup") - } else { - self.licenseStatus = .licensed(userName: userName, email: email) - print("โœ… LICENSE: Production license verified on startup") - } - - self.isLicenseValid = true - self.saveDate(Date(), forKey: self.lastVerificationKey) - } else { - // License might be revoked, disputed, etc. - self.licenseStatus = .invalid - self.isLicenseValid = false - // Clear stored license - self.deleteKey(self.licenseKeyKey) - self.deleteKey(self.userNameKey) - self.deleteKey(self.userEmailKey) - self.deleteKey(self.isTestLicenseKey) - } - } - } - - private func verifyLicenseWithGumroad(licenseKey: String, userName: String) async -> Bool { - guard let url = URL(string: gumroadAPIURL) else { - print("โŒ LICENSE: Invalid Gumroad API URL") - return false - } - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - - // Prepare form data - var formData = [String]() - formData.append("product_id=\(productID)") - formData.append("license_key=\(licenseKey)") - formData.append("increment_uses_count=false") // Don't increment on every check - - let bodyString = formData.joined(separator: "&") - request.httpBody = bodyString.data(using: .utf8) - - do { - let (data, response) = try await URLSession.shared.data(for: request) - - if let httpResponse = response as? HTTPURLResponse { - print("๐Ÿ” LICENSE: Gumroad API response status: \(httpResponse.statusCode)") - } - - let verifyResponse = try JSONDecoder().decode(GumroadVerifyResponse.self, from: data) - - if verifyResponse.success { - // Additional checks - if let purchase = verifyResponse.purchase { - // ๐Ÿงช Check if this is a test purchase - let isTestPurchase = purchase.test == true - - if isTestPurchase { - print("๐Ÿงช LICENSE: Test purchase detected!") - print("๐Ÿงช LICENSE: Product: \(purchase.product_name ?? "Unknown")") - print("๐Ÿงช LICENSE: Email: \(purchase.email ?? "Unknown")") - print("๐Ÿงช LICENSE: Price: \(purchase.price ?? 0) \(purchase.currency ?? "USD")") - print("๐Ÿงช LICENSE: This is a development/test license") - } - - // Check if refunded, disputed, or chargebacked - if purchase.refunded == true || purchase.disputed == true || purchase.chargebacked == true { - print("โŒ LICENSE: License is refunded, disputed, or chargebacked") - return false - } - - // Store additional info - if let email = purchase.email { - saveString(email, forKey: userEmailKey) - } - - // Store test status for UI display - saveBool(isTestPurchase, forKey: isTestLicenseKey) - - print("โœ… LICENSE: Gumroad verification successful") - print("๐Ÿ” LICENSE: Product: \(purchase.product_name ?? "Unknown")") - print("๐Ÿ” LICENSE: Email: \(purchase.email ?? "Unknown")") - print("๐Ÿ” LICENSE: License Type: \(isTestPurchase ? "TEST" : "PRODUCTION")") - return true - } - } else { - print("โŒ LICENSE: Gumroad verification failed: \(verifyResponse.message ?? "Unknown error")") - } - - } catch { - print("โŒ LICENSE: Gumroad API error: \(error.localizedDescription)") - } - - return false - } - - private func getStoredUserName() -> String { - return loadString(forKey: userNameKey) ?? "Unknown User" - } - - private func getStoredEmail() -> String { - return loadString(forKey: userEmailKey) ?? "" - } -} - -// MARK: - Notification Names -extension Notification.Name { - static let licenseStatusChanged = Notification.Name("licenseStatusChanged") -} \ No newline at end of file diff --git a/ShotScreen/Sources/MenuManager.swift b/ShotScreen/Sources/MenuManager.swift deleted file mode 100644 index 787a36b..0000000 --- a/ShotScreen/Sources/MenuManager.swift +++ /dev/null @@ -1,783 +0,0 @@ -import AppKit - -// MARK: - Menu Management -class MenuManager { - weak var delegate: MenuManagerDelegate? - private var statusItem: NSStatusItem? - - init(delegate: MenuManagerDelegate) { - self.delegate = delegate - - // Add observer for shortcut changes to update menu - NotificationCenter.default.addObserver( - self, - selector: #selector(updateShortcuts), - name: .shortcutSettingChanged, - object: nil - ) - - // Add observer for desktop icons setting changes - NotificationCenter.default.addObserver( - self, - selector: #selector(handleDesktopIconsSettingChanged), - name: .hideDesktopIconsSettingChanged, - object: nil - ) - - // Add observer for desktop widgets setting changes - NotificationCenter.default.addObserver( - self, - selector: #selector(handleDesktopWidgetsSettingChanged), - name: .hideDesktopWidgetsSettingChanged, - object: nil - ) - } - - func setupMenu() { - setupMainMenu() - setupStatusBarMenu() - - // Set initial shortcuts - updateShortcuts() - } - - private func setupMainMenu() { - // Create main menu - let mainMenu = NSMenu() - - // Application menu (first menu) - let appMenu = NSMenu() - let appName = "ShotScreen" - - // About item with info icon - let aboutItem = NSMenuItem(title: "About \(appName)", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: "") - if let infoIcon = NSImage(systemSymbolName: "info.circle", accessibilityDescription: "About") { - infoIcon.size = NSSize(width: 16, height: 16) - aboutItem.image = infoIcon - } - appMenu.addItem(aboutItem) - appMenu.addItem(NSMenuItem.separator()) - - // Settings item with gear icon - let settingsItem = NSMenuItem(title: "Settings", action: #selector(MenuManager.openSettings), keyEquivalent: ",") - settingsItem.target = self - if let gearIcon = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "Settings") { - gearIcon.size = NSSize(width: 16, height: 16) - settingsItem.image = gearIcon - } - appMenu.addItem(settingsItem) - - // Show Stash item with folder icon - let showStashItem = NSMenuItem(title: "Show Stash", action: #selector(MenuManager.showStash), keyEquivalent: "s") - showStashItem.target = self - if let folderIcon = NSImage(systemSymbolName: "folder", accessibilityDescription: "Folder") { - folderIcon.size = NSSize(width: 16, height: 16) - showStashItem.image = folderIcon - } - appMenu.addItem(showStashItem) - - // NIEUW: Reset First Launch Wizard (for testing) - appMenu.addItem(NSMenuItem.separator()) - let resetWizardItem = NSMenuItem(title: "Reset First Launch Wizard", action: #selector(MenuManager.resetFirstLaunchWizard), keyEquivalent: "") - resetWizardItem.target = self - if let resetIcon = NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: "Reset") { - resetIcon.size = NSSize(width: 16, height: 16) - resetWizardItem.image = resetIcon - } - appMenu.addItem(resetWizardItem) - - appMenu.addItem(NSMenuItem.separator()) - - // Quit item with power icon - let quitItem = NSMenuItem(title: "Quit \(appName)", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q") - if let powerIcon = NSImage(systemSymbolName: "power", accessibilityDescription: "Quit") { - powerIcon.size = NSSize(width: 16, height: 16) - quitItem.image = powerIcon - } - appMenu.addItem(quitItem) - - // Add app menu to main menu - let appMenuItem = NSMenuItem(title: appName, action: nil, keyEquivalent: "") - appMenuItem.submenu = appMenu - mainMenu.addItem(appMenuItem) - - // Set the menu - NSApplication.shared.mainMenu = mainMenu - } - - private func setupStatusBarMenu() { - // Create status item in menu bar - statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) - if let button = statusItem?.button { - // Use MenuIcon for status bar instead of AppIcon - if let bundle = Bundle.main.url(forResource: "MenuIcon", withExtension: "png", subdirectory: "images"), - let menuIcon = NSImage(contentsOf: bundle) { - menuIcon.size = NSSize(width: 16, height: 16) - button.image = menuIcon - } else if let menuIcon = NSImage(named: "MenuIcon") { - menuIcon.size = NSSize(width: 16, height: 16) - button.image = menuIcon - } else if let screenshotIcon = NSImage(systemSymbolName: "camera.aperture", accessibilityDescription: "ShotScreen") { - // Fallback to aperture icon if menu icon not available - screenshotIcon.size = NSSize(width: 16, height: 16) - button.image = screenshotIcon - } else { - // Final fallback to green dot - button.image = NSImage(named: "NSStatusAvailable") - } - - // Debug: Print all status bar items - debugPrintStatusBarItems() - - // Create a menu for the status item - let statusMenu = NSMenu() - - // Take Screenshot menu item with camera icon - let screenshotItem = NSMenuItem(title: "Take Screenshot", action: #selector(MenuManager.triggerScreenshot), keyEquivalent: "") - screenshotItem.target = self - if let cameraIcon = NSImage(systemSymbolName: "camera", accessibilityDescription: "Camera") { - cameraIcon.size = NSSize(width: 16, height: 16) - screenshotItem.image = cameraIcon - } - updateScreenshotShortcut(screenshotItem) // Set initial shortcut - statusMenu.addItem(screenshotItem) - - // NIEUW: Capture Whole Screen (Double Hotkey) menu item with monitor icon - let wholeScreenItem = NSMenuItem(title: "Capture This Screen", action: #selector(MenuManager.triggerWholeScreenCapture), keyEquivalent: "") - wholeScreenItem.target = self - if let monitorIcon = NSImage(systemSymbolName: "display", accessibilityDescription: "Monitor") { - monitorIcon.size = NSSize(width: 16, height: 16) - wholeScreenItem.image = monitorIcon - } - updateWholeScreenCaptureShortcut(wholeScreenItem) // Set initial shortcut - statusMenu.addItem(wholeScreenItem) - - // NIEUW: Capture All Screens menu item - let allScreensItem = NSMenuItem(title: "Capture All Screens", action: #selector(MenuManager.triggerAllScreensCapture), keyEquivalent: "") - allScreensItem.target = self - if let screensIcon = NSImage(systemSymbolName: "rectangle.3.group", accessibilityDescription: "All Screens") { - screensIcon.size = NSSize(width: 16, height: 16) - allScreensItem.image = screensIcon - } - updateAllScreensCaptureShortcut(allScreensItem) // Set initial shortcut - statusMenu.addItem(allScreensItem) - - // NIEUW: Simple Window Capture menu item (native macOS) - let windowCaptureItem = NSMenuItem(title: "Capture Window", action: #selector(MenuManager.triggerNativeWindowCapture), keyEquivalent: "") - windowCaptureItem.target = self - if let windowIcon = NSImage(systemSymbolName: "macwindow", accessibilityDescription: "Window") { - windowIcon.size = NSSize(width: 16, height: 16) - windowCaptureItem.image = windowIcon - } - updateWindowCaptureShortcut(windowCaptureItem) // Set initial shortcut display - statusMenu.addItem(windowCaptureItem) - - // Add separator between screenshot actions and other options - statusMenu.addItem(NSMenuItem.separator()) - - // NIEUW: Toggle Desktop Icons menu item - let hideIconsItem = NSMenuItem(title: "", action: #selector(MenuManager.toggleDesktopIcons), keyEquivalent: "") - hideIconsItem.target = self - updateDesktopIconsMenuItem(hideIconsItem) // Set initial title and icon - statusMenu.addItem(hideIconsItem) - print("โž• Desktop Icons menu item added with initial state") - - // NIEUW: Toggle Desktop Widgets menu item - let hideWidgetsItem = NSMenuItem(title: "", action: #selector(MenuManager.toggleDesktopWidgets), keyEquivalent: "") - hideWidgetsItem.target = self - updateDesktopWidgetsMenuItem(hideWidgetsItem) // Set initial title and icon - statusMenu.addItem(hideWidgetsItem) - print("โž• Desktop Widgets menu item added with initial state") - - // Add separator between screenshot actions and other options - statusMenu.addItem(NSMenuItem.separator()) - - // Show Stash menu item with folder icon - let stashItem = NSMenuItem(title: "Show Stash", action: #selector(MenuManager.showStash), keyEquivalent: "") - stashItem.target = self - if let folderIcon = NSImage(systemSymbolName: "folder", accessibilityDescription: "Folder") { - folderIcon.size = NSSize(width: 16, height: 16) - stashItem.image = folderIcon - } - statusMenu.addItem(stashItem) - - - - // ๐Ÿ”„ UPDATE: Check for Updates menu item with arrow icon - let updatesItem = NSMenuItem(title: "Check for Updates", action: #selector(MenuManager.checkForUpdates), keyEquivalent: "") - updatesItem.target = self - updatesItem.isEnabled = UpdateManager.shared.isUpdaterAvailable - if let updateIcon = NSImage(systemSymbolName: "arrow.clockwise.circle", accessibilityDescription: "Check for Updates") { - updateIcon.size = NSSize(width: 16, height: 16) - updatesItem.image = updateIcon - } - - - statusMenu.addItem(NSMenuItem.separator()) - - // NIEUW: Reset First Launch Wizard (for testing) - also in status bar menu - let resetWizardItem = NSMenuItem(title: "Reset First Launch Wizard", action: #selector(MenuManager.resetFirstLaunchWizard), keyEquivalent: "") - resetWizardItem.target = self - if let resetIcon = NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: "Reset") { - resetIcon.size = NSSize(width: 16, height: 16) - resetWizardItem.image = resetIcon - } - statusMenu.addItem(resetWizardItem) - - statusMenu.addItem(NSMenuItem.separator()) - // Settings menu item with gear icon - let settingsItem = NSMenuItem(title: "Settings", action: #selector(MenuManager.openSettings), keyEquivalent: "") - settingsItem.target = self - if let gearIcon = NSImage(systemSymbolName: "gearshape", accessibilityDescription: "Settings") { - gearIcon.size = NSSize(width: 16, height: 16) - settingsItem.image = gearIcon - } - statusMenu.addItem(settingsItem) - // Quit menu item with power icon - let quitItem = NSMenuItem(title: "Quit ShotScreen", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "") - if let powerIcon = NSImage(systemSymbolName: "power", accessibilityDescription: "Quit") { - powerIcon.size = NSSize(width: 16, height: 16) - quitItem.image = powerIcon - } - statusMenu.addItem(quitItem) - - statusItem?.menu = statusMenu - } - } - - // MARK: - Menu Actions - @objc func triggerScreenshot() { - delegate?.triggerScreenshot() - } - - @objc func triggerWholeScreenCapture() { - delegate?.triggerWholeScreenCapture() - } - - @objc func triggerNativeWindowCapture() { - delegate?.triggerNativeWindowCapture() - } - - @objc func triggerAllScreensCapture() { - delegate?.triggerAllScreensCapture() - } - - @objc func openSettings() { - delegate?.openSettings() - } - - @objc func showStash() { - delegate?.showStash() - } - - @objc func resetFirstLaunchWizard() { - delegate?.resetFirstLaunchWizard() - } - - @objc func toggleDesktopIcons() { - delegate?.toggleDesktopIcons() - } - - @objc func toggleDesktopWidgets() { - delegate?.toggleDesktopWidgets() - } - - @objc func checkForUpdates() { - delegate?.checkForUpdates() - } - - // MARK: - Dynamic Version Helper - private func getDynamicAboutTitle() -> String { - let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" - return "ShotScreen v\(version)" - } - - @objc func showAbout() { - delegate?.showAbout() - } - - @objc func exitApp() { - delegate?.shouldTerminate = true - NSApplication.shared.terminate(self) - } - - // MARK: - Menu Item Updates - private func updateDesktopIconsMenuItem(_ menuItem: NSMenuItem) { - let isHidden = SettingsManager.shared.hideDesktopIconsDuringScreenshot - - if isHidden { - menuItem.title = "Show Desktop Icons" - // Try specific icon first, fallback to generic folder icon - if let icon = NSImage(systemSymbolName: "folder.badge.minus", accessibilityDescription: "Desktop Icons Hidden") { - icon.size = NSSize(width: 16, height: 16) - menuItem.image = icon - } else if let fallbackIcon = NSImage(systemSymbolName: "folder", accessibilityDescription: "Folder") { - fallbackIcon.size = NSSize(width: 16, height: 16) - menuItem.image = fallbackIcon - } else { - menuItem.image = nil - } - } else { - menuItem.title = "Hide Desktop Icons" - // Try specific icon first, fallback to generic folder icon - if let icon = NSImage(systemSymbolName: "folder.badge.plus", accessibilityDescription: "Desktop Icons Visible") { - icon.size = NSSize(width: 16, height: 16) - menuItem.image = icon - } else if let fallbackIcon = NSImage(systemSymbolName: "folder", accessibilityDescription: "Folder") { - fallbackIcon.size = NSSize(width: 16, height: 16) - menuItem.image = fallbackIcon - } else { - menuItem.image = nil - } - } - - print("๐Ÿ–ฅ๏ธ Desktop Icons menu item updated: '\(menuItem.title)' (Hidden: \(isHidden))") - } - - private func updateDesktopWidgetsMenuItem(_ menuItem: NSMenuItem) { - let isHidden = SettingsManager.shared.hideDesktopWidgetsDuringScreenshot - - if isHidden { - menuItem.title = "Show Desktop Widgets" - // Try specific widget icons first, fallback to simpler rectangle icon - if let icon = NSImage(systemSymbolName: "rectangle.3.group.bubble", accessibilityDescription: "Desktop Widgets Hidden") { - icon.size = NSSize(width: 16, height: 16) - menuItem.image = icon - } else if let fallbackIcon = NSImage(systemSymbolName: "rectangle.3.group", accessibilityDescription: "Widgets") { - fallbackIcon.size = NSSize(width: 16, height: 16) - menuItem.image = fallbackIcon - } else if let simpleIcon = NSImage(systemSymbolName: "rectangle.grid.2x2", accessibilityDescription: "Grid") { - simpleIcon.size = NSSize(width: 16, height: 16) - menuItem.image = simpleIcon - } else { - menuItem.image = nil - } - } else { - menuItem.title = "Hide Desktop Widgets" - // Try specific widget icons first, fallback to simpler rectangle icon - if let icon = NSImage(systemSymbolName: "rectangle.3.group.bubble.fill", accessibilityDescription: "Desktop Widgets Visible") { - icon.size = NSSize(width: 16, height: 16) - menuItem.image = icon - } else if let fallbackIcon = NSImage(systemSymbolName: "rectangle.3.group.fill", accessibilityDescription: "Widgets") { - fallbackIcon.size = NSSize(width: 16, height: 16) - menuItem.image = fallbackIcon - } else if let simpleIcon = NSImage(systemSymbolName: "rectangle.grid.2x2.fill", accessibilityDescription: "Grid") { - simpleIcon.size = NSSize(width: 16, height: 16) - menuItem.image = simpleIcon - } else { - menuItem.image = nil - } - } - - print("๐Ÿ“ฑ Desktop Widgets menu item updated: '\(menuItem.title)' (Hidden: \(isHidden))") - } - - // Public method to refresh menu when settings change - func refreshDesktopIconsMenuItem() { - print("๐Ÿ”„ Refreshing desktop icons menu item...") - if let menu = statusItem?.menu { - // Find the desktop icons menu item (it should be the one with our action) - var foundItem = false - for item in menu.items { - if item.action == #selector(MenuManager.toggleDesktopIcons) { - updateDesktopIconsMenuItem(item) - foundItem = true - break - } - } - if !foundItem { - print("โš ๏ธ Desktop icons menu item not found!") - } - } else { - print("โš ๏ธ Status item menu not available!") - } - } - - // Public method to refresh widgets menu when settings change - func refreshDesktopWidgetsMenuItem() { - print("๐Ÿ”„ Refreshing desktop widgets menu item...") - if let menu = statusItem?.menu { - // Find the desktop widgets menu item (it should be the one with our action) - var foundItem = false - for item in menu.items { - if item.action == #selector(MenuManager.toggleDesktopWidgets) { - updateDesktopWidgetsMenuItem(item) - foundItem = true - break - } - } - if !foundItem { - print("โš ๏ธ Desktop widgets menu item not found!") - } - } else { - print("โš ๏ธ Status item menu not available!") - } - } - - // MARK: - Settings Change Handlers - @objc private func handleDesktopIconsSettingChanged() { - print("๐Ÿ“ข Notification received: Desktop Icons setting changed") - DispatchQueue.main.async { [weak self] in - self?.refreshDesktopIconsMenuItem() - } - } - - @objc private func handleDesktopWidgetsSettingChanged() { - print("๐Ÿ“ข Notification received: Desktop Widgets setting changed") - DispatchQueue.main.async { [weak self] in - self?.refreshDesktopWidgetsMenuItem() - } - } - - // MARK: - Shortcut Management - @objc func updateShortcuts() { - // Update shortcuts in both main menu and status bar menu - updateMenuShortcuts() - } - - private func updateMenuShortcuts() { - // Update status bar menu shortcuts - if let menu = statusItem?.menu { - for item in menu.items { - if item.action == #selector(MenuManager.triggerScreenshot) { - updateScreenshotShortcut(item) - } else if item.action == #selector(MenuManager.triggerWholeScreenCapture) { - updateWholeScreenCaptureShortcut(item) - } else if item.action == #selector(MenuManager.triggerNativeWindowCapture) { - updateWindowCaptureShortcut(item) - } else if item.action == #selector(MenuManager.triggerAllScreensCapture) { - updateAllScreensCaptureShortcut(item) - } - } - } - - // Update main menu shortcuts if needed - updateMainMenuShortcuts() - } - - private func updateMainMenuShortcuts() { - // Update shortcuts in the main application menu - if let mainMenu = NSApplication.shared.mainMenu { - // Find the application menu (first submenu) - if let appMenuItem = mainMenu.items.first, - let appSubmenu = appMenuItem.submenu { - for item in appSubmenu.items { - if item.action == #selector(MenuManager.openSettings) { - // Settings already has "," shortcut, keep it - break - } - } - } - } - } - - private func updateScreenshotShortcut(_ menuItem: NSMenuItem) { - let settings = SettingsManager.shared - - if settings.useCustomShortcut && settings.customShortcutModifiers != 0 && settings.customShortcutKey != 0 { - // Use custom shortcut - display only, same as others - let shortcutText = getShortcutDisplay(modifiers: settings.customShortcutModifiers, keyCode: settings.customShortcutKey) - setMenuItemWithRightAlignedShortcut(menuItem, title: "Take Area Screenshot", shortcut: shortcutText) - } else { - // Use default Cmd+Shift+4 - display only, positioned slightly to the left - setMenuItemWithRightAlignedShortcut(menuItem, title: "Take Area Screenshot", shortcut: "โŒ˜โ‡ง4") - } - - // Clear actual key equivalents since these are display-only - menuItem.keyEquivalent = "" - menuItem.keyEquivalentModifierMask = [] - } - - private func updateWholeScreenCaptureShortcut(_ menuItem: NSMenuItem) { - let settings = SettingsManager.shared - let baseShortcut: String - - if settings.useCustomShortcut && settings.customShortcutModifiers != 0 && settings.customShortcutKey != 0 { - // Use custom shortcut - baseShortcut = getShortcutDisplay(modifiers: settings.customShortcutModifiers, keyCode: settings.customShortcutKey) - } else { - // Use default shortcut - baseShortcut = "โŒ˜โ‡ง4" - } - - // Display shortcut as: 2 x [shortcut] (double hotkey for whole screen) - setMenuItemWithRightAlignedShortcut(menuItem, title: "Capture Screen", shortcut: "2 x \(baseShortcut)") - - // Clear actual key equivalents since these are display-only - menuItem.keyEquivalent = "" - menuItem.keyEquivalentModifierMask = [] - } - - private func updateWindowCaptureShortcut(_ menuItem: NSMenuItem) { - let settings = SettingsManager.shared - let baseShortcut: String - - if settings.useCustomShortcut && settings.customShortcutModifiers != 0 && settings.customShortcutKey != 0 { - // Use custom shortcut - baseShortcut = getShortcutDisplay(modifiers: settings.customShortcutModifiers, keyCode: settings.customShortcutKey) - } else { - // Use default shortcut - baseShortcut = "โŒ˜โ‡ง4" - } - - // Display shortcut as: [shortcut] โ†’ โฃ (indicating spacebar during selection) - setMenuItemWithRightAlignedShortcut(menuItem, title: "Capture Window", shortcut: "\(baseShortcut) โ†’ โฃ") - - // Clear actual key equivalents since these are display-only - menuItem.keyEquivalent = "" - menuItem.keyEquivalentModifierMask = [] - } - - private func updateAllScreensCaptureShortcut(_ menuItem: NSMenuItem) { - let settings = SettingsManager.shared - let baseShortcut: String - - if settings.useCustomShortcut && settings.customShortcutModifiers != 0 && settings.customShortcutKey != 0 { - // Use custom shortcut - baseShortcut = getShortcutDisplay(modifiers: settings.customShortcutModifiers, keyCode: settings.customShortcutKey) - } else { - // Use default shortcut - baseShortcut = "โŒ˜โ‡ง4" - } - - // Triple hotkey for all screens capture - setMenuItemWithRightAlignedShortcut(menuItem, title: "Capture All Screens", shortcut: "3 x \(baseShortcut)") - - // Clear actual key equivalents since these are display-only - menuItem.keyEquivalent = "" - menuItem.keyEquivalentModifierMask = [] - } - - private func setMenuItemWithRightAlignedShortcut(_ menuItem: NSMenuItem, title: String, shortcut: String) { - // Create attributed string with proper tab stop for right alignment - let attributedTitle = NSMutableAttributedString() - - // Create paragraph style with right-aligned tab stop - let paragraphStyle = NSMutableParagraphStyle() - - // Calculate the right tab stop position (approximately 250 points from left margin) - // This gives us consistent right alignment for all shortcuts - let tabStopPosition: CGFloat = 250.0 - - // Create a right-aligned tab stop - let tabStop = NSTextTab(textAlignment: .right, location: tabStopPosition, options: [:]) - paragraphStyle.tabStops = [tabStop] - - // Add the main title with normal styling - let titleString = NSAttributedString(string: title, attributes: [ - .font: NSFont.menuFont(ofSize: 0), - .paragraphStyle: paragraphStyle - ]) - attributedTitle.append(titleString) - - // Add tab character to move to the tab stop - let tabString = NSAttributedString(string: "\t", attributes: [ - .paragraphStyle: paragraphStyle - ]) - attributedTitle.append(tabString) - - // Add the shortcut with different styling at the tab stop (right-aligned) - let shortcutString = NSAttributedString(string: shortcut, attributes: [ - .foregroundColor: NSColor.secondaryLabelColor, - .font: NSFont.menuFont(ofSize: 0), - .paragraphStyle: paragraphStyle - ]) - attributedTitle.append(shortcutString) - - menuItem.attributedTitle = attributedTitle - } - - private func getShortcutDisplay(modifiers: UInt, keyCode: UInt16) -> String { - var result = "" - - // Add modifier symbols in correct order: Control, Option, Shift, Command - if modifiers & (1 << 3) != 0 { result += "โŒƒ" } // Control - if modifiers & (1 << 2) != 0 { result += "โŒฅ" } // Option - if modifiers & (1 << 1) != 0 { result += "โ‡ง" } // Shift - if modifiers & (1 << 0) != 0 { result += "โŒ˜" } // Command - - let keyString = keyCodeToDisplayString(keyCode) - result += keyString - - return result - } - - private func keyCodeToDisplayString(_ keyCode: UInt16) -> String { - // Convert keyCode to display string (uppercase for better readability) - switch keyCode { - case 0: return "A" - case 1: return "S" - case 2: return "D" - case 3: return "F" - case 4: return "H" - case 5: return "G" - case 6: return "Z" - case 7: return "X" - case 8: return "C" - case 9: return "V" - case 11: return "B" - case 12: return "Q" - case 13: return "W" - case 14: return "E" - case 15: return "R" - case 16: return "Y" - case 17: return "T" - case 18: return "1" - case 19: return "2" - case 20: return "3" - case 21: return "4" - case 22: return "6" - case 23: return "5" - case 24: return "=" - case 25: return "9" - case 26: return "7" - case 27: return "-" - case 28: return "8" - case 29: return "0" - case 30: return "]" - case 31: return "O" - case 32: return "U" - case 33: return "[" - case 34: return "I" - case 35: return "P" - case 37: return "L" - case 38: return "J" - case 39: return "'" - case 40: return "K" - case 41: return ";" - case 42: return "\\" - case 43: return "," - case 44: return "/" - case 45: return "N" - case 46: return "M" - case 47: return "." - case 50: return "`" - default: return "" - } - } - - private func getModifierSymbols(_ modifier: UInt) -> String { - var symbols: [String] = [] - - if modifier & (1 << 3) != 0 { symbols.append("โŒƒ") } // Control - if modifier & (1 << 2) != 0 { symbols.append("โฃ") } // Option (now Space symbol) - if modifier & (1 << 1) != 0 { symbols.append("โ‡ง") } // Shift - if modifier & (1 << 0) != 0 { symbols.append("โŒ˜") } // Command - - return symbols.joined() - } - - // MARK: - License Management - func setAppEnabled(_ enabled: Bool) { - print("๐Ÿ” LICENSE: Setting app menu enabled state to: \(enabled)") - - guard let menu = statusItem?.menu else { - print("โš ๏ธ LICENSE: Status item menu not available!") - return - } - - // Disable/enable screenshot-related menu items - for item in menu.items { - if item.action == #selector(MenuManager.triggerScreenshot) || - item.action == #selector(MenuManager.triggerWholeScreenCapture) || - item.action == #selector(MenuManager.triggerNativeWindowCapture) || - item.action == #selector(MenuManager.triggerAllScreensCapture) || - item.action == #selector(MenuManager.showStash) || - item.action == #selector(MenuManager.toggleDesktopIcons) || - item.action == #selector(MenuManager.toggleDesktopWidgets) { - item.isEnabled = enabled - } - // Keep Settings, Updates, About, and Quit always enabled - } - - // Update menu icon to indicate license status - if !enabled { - // Show different icon for trial/expired - use lock icon - if let lockIcon = NSImage(systemSymbolName: "lock.fill", accessibilityDescription: "License Required") { - lockIcon.size = NSSize(width: 16, height: 16) - statusItem?.button?.image = lockIcon - } - statusItem?.button?.toolTip = "ShotScreen - License Required" - } else { - // Restore normal menu icon for licensed - if let bundle = Bundle.main.url(forResource: "MenuIcon", withExtension: "png", subdirectory: "images"), - let menuIcon = NSImage(contentsOf: bundle) { - menuIcon.size = NSSize(width: 16, height: 16) - statusItem?.button?.image = menuIcon - } else if let menuIcon = NSImage(named: "MenuIcon") { - menuIcon.size = NSSize(width: 16, height: 16) - statusItem?.button?.image = menuIcon - } else if let screenshotIcon = NSImage(systemSymbolName: "camera.aperture", accessibilityDescription: "ShotScreen") { - screenshotIcon.size = NSSize(width: 16, height: 16) - statusItem?.button?.image = screenshotIcon - } - statusItem?.button?.toolTip = "ShotScreen" - } - - // Clear any title to prevent duplicate icons - statusItem?.button?.title = "" - } - - // MARK: - Cleanup - func cleanup() { - print("๐Ÿงน MenuManager: Cleaning up...") - NotificationCenter.default.removeObserver(self) - - if let item = statusItem { - NSStatusBar.system.removeStatusItem(item) - print("๐Ÿงน STATUS: Removed status bar item successfully") - } - statusItem = nil - } - - deinit { - print("๐Ÿ—‘๏ธ MenuManager: Deinitializing and cleaning up") - cleanup() - } - - private func debugPrintStatusBarItems() { - print("๐Ÿ” DEBUG: Checking all status bar items...") - - // Try to get information about other status bar items - let statusBar = NSStatusBar.system - print("๐Ÿ” DEBUG: System status bar available: \(statusBar)") - - // Check if there might be multiple ShotScreen processes running - let runningApps = NSWorkspace.shared.runningApplications - let shotScreenApps = runningApps.filter { $0.bundleIdentifier?.contains("ShotScreen") == true || $0.localizedName?.contains("ShotScreen") == true } - - print("๐Ÿ” DEBUG: Found \(shotScreenApps.count) ShotScreen-related processes:") - for app in shotScreenApps { - print(" - \(app.localizedName ?? "Unknown") (PID: \(app.processIdentifier))") - } - - // Check for screenshot-related apps - let screenshotApps = runningApps.filter { - let name = $0.localizedName?.lowercased() ?? "" - let bundle = $0.bundleIdentifier?.lowercased() ?? "" - return name.contains("screenshot") || name.contains("capture") || name.contains("screen") || - bundle.contains("screenshot") || bundle.contains("capture") || bundle.contains("screen") - } - - print("๐Ÿ” DEBUG: Found \(screenshotApps.count) screenshot/capture-related apps:") - for app in screenshotApps { - print(" - \(app.localizedName ?? "Unknown"): \(app.bundleIdentifier ?? "No Bundle ID")") - } - } -} - -// MARK: - MenuManager Delegate Protocol -protocol MenuManagerDelegate: AnyObject { - func triggerScreenshot() - func triggerWholeScreenCapture() - func triggerNativeWindowCapture() - func triggerAllScreensCapture() - func openSettings() - func showStash() - func resetFirstLaunchWizard() - func toggleDesktopIcons() - func toggleDesktopWidgets() - func checkForUpdates() - func showAbout() - var shouldTerminate: Bool { get set } -} \ No newline at end of file diff --git a/ShotScreen/Sources/MultiMonitorSystem.swift b/ShotScreen/Sources/MultiMonitorSystem.swift deleted file mode 100644 index 458c2c9..0000000 --- a/ShotScreen/Sources/MultiMonitorSystem.swift +++ /dev/null @@ -1,1263 +0,0 @@ -import AppKit -import SwiftUI - -// MARK: - Multi-Monitor Screenshot System -extension ScreenshotApp { - - // MARK: - Global Mouse Tracking Setup - func setupGlobalMouseTracking() { - // Global monitors (for events outside our app) - globalMouseDownMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown]) { [weak self] event in - self?.handleGlobalMouseDown(event) - } - - globalMouseDragMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDragged]) { [weak self] event in - self?.handleGlobalMouseDragged(event) - } - - globalMouseUpMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseUp]) { [weak self] event in - self?.handleGlobalMouseUp(event) - } - - // Local monitors (for events within our app) - these can consume events - localMouseDownMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown]) { [weak self] event in - guard let self = self else { return event } - - if self.isMultiMonitorSelectionActive && !self.isDragging { - let globalLocation = NSEvent.mouseLocation - - // SET MOUSE TRACKING VARIABLES FOR SINGLE CLICK DETECTION - let allScreenModifier: UInt = (1 << 0) // Command key - self.isAllScreenModifierPressed = self.isModifierPressed(event.modifierFlags, modifier: allScreenModifier) - self.mouseDownLocation = globalLocation - self.mouseDownTime = CACurrentMediaTime() - self.hasMouseMoved = false - - let modifierName = self.getModifierName(allScreenModifier) - print("๐ŸŽฏ Local mouse down - starting selection at: \(globalLocation), \(modifierName): \(self.isAllScreenModifierPressed)") - self.startDragSelection(at: globalLocation) - return nil // Consume the event to prevent other apps from processing it - } - - return event // Let other apps handle it normally - } - - localMouseDragMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDragged]) { [weak self] event in - guard let self = self else { return event } - - if self.isMultiMonitorSelectionActive && self.isDragging { - let globalLocation = NSEvent.mouseLocation - self.hasMouseMoved = true // MARK THAT MOUSE HAS MOVED - self.updateDragSelection(to: globalLocation) - return nil // LOCAL monitors return NSEvent? - } - - return event - } - - localMouseUpMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseUp]) { [weak self] event in - guard let self = self else { return event } - - if self.isMultiMonitorSelectionActive && self.isDragging { - let globalLocation = NSEvent.mouseLocation - - // CHECK FOR SINGLE CLICK FIRST - let timeSinceMouseDown = CACurrentMediaTime() - self.mouseDownTime - let distanceMoved = sqrt(pow(globalLocation.x - self.mouseDownLocation.x, 2) + pow(globalLocation.y - self.mouseDownLocation.y, 2)) - - print("๐ŸŽฏ Local mouse up at: \(globalLocation)") - print("๐ŸŽฏ Time since down: \(timeSinceMouseDown)s, Distance: \(distanceMoved)px, HasMouseMoved: \(self.hasMouseMoved)") - print("๐ŸŽฏ isAllScreensCaptureToggledOn: \(self.isAllScreensCaptureToggledOn), isAllScreenModifierPressed (at mouseDown): \(self.isAllScreenModifierPressed), isWindowCaptureMode: \(self.isWindowCaptureMode)") - - // Was it a click (not a drag)? - let isClick = !self.hasMouseMoved && distanceMoved < 5.0 && timeSinceMouseDown < 0.5 - print("๐ŸŽฏ Evaluated isClick: \(isClick)") - - if isClick { - // 1. If "all screens" mode is GETOGGLED ON - if self.isAllScreensCaptureToggledOn { - print("๐ŸŽฏ Condition MET: isAllScreensCaptureToggledOn is TRUE.") - print("๐ŸŽฏ Local Click detected (toggle ON) - capturing all screens") - self.deactivateMultiMonitorSelection() - self.captureAllScreens() // captureAllScreens resets de toggle - self.resetTrackingVariables() - return event // Return event since this is a local monitor - } - // 2. If it was a single click (no toggle, no CMD modifier pressed) - else if !self.isAllScreenModifierPressed { - // IMPORTANT: Don't capture screen if we're in window capture mode - if self.isWindowCaptureMode { - print("๐ŸŽฏ Condition SKIPPED (isAllScreensCaptureToggledOn=false): Single click in window capture mode - ignoring screen capture") - return event // Let event continue in window capture mode - } - - print("๐ŸŽฏ Condition MET (isAllScreensCaptureToggledOn=false, !isAllScreenModifierPressed): Single click detected (toggle OFF) - capturing current screen") - self.deactivateMultiMonitorSelection() - self.captureCurrentScreen(at: globalLocation) - self.resetTrackingVariables() - return nil // Consume event - } - // 3. If it was CMD+click (modifier held, setting enabled) - else if self.isAllScreenModifierPressed { - let allScreenModifier: UInt = (1 << 0) // Command key - let modifierName = self.getModifierName(allScreenModifier) - print("๐ŸŽฏ Condition MET (isAllScreensCaptureToggledOn=false, isAllScreenModifierPressed): \(modifierName)+click detected - capturing all screens") - self.deactivateMultiMonitorSelection() - self.captureAllScreens() - self.resetTrackingVariables() - return nil // Consume event - } - else { - print("๐ŸŽฏ Condition NOT MET for any click type when isClick=true. isAllScreensCaptureToggledOn=\(self.isAllScreensCaptureToggledOn), isAllScreenModifierPressed=\(self.isAllScreenModifierPressed)") - } - } - else { - print("๐ŸŽฏ Evaluated isClick as FALSE. Proceeding with drag logic or unhandled mouse up.") - } - - // If it wasn't a click handled above, or it was a drag completion - print("๐ŸŽฏ Local mouse up - ending drag selection normally or unhandled click.") - self.endDragSelection(at: globalLocation) - // For drag completion, endDragSelection handles deactivation and capture. - // We can return nil here as well, as endDragSelection is the final action for a drag. - // Or return event if subsequent handlers (like EventCaptureView.mouseUp) are desired for drag completion. - // Let's return nil to make localMouseUpMonitor authoritative for drags too. - return nil - } - - return event - } - - // ESC key monitor for canceling selection - NSEvent.addLocalMonitorForEvents(matching: [.keyDown]) { [weak self] event in - if event.keyCode == 53 { // ESC key - if self?.isMultiMonitorSelectionActive == true { - print("โŒจ๏ธ ESC pressed - canceling multi-monitor selection") - self?.cancelMultiMonitorSelection() - return nil // LOCAL monitors return NSEvent? - } - } - return event - } - - print("๐ŸŽฏ Global mouse tracking setup complete") - } - - // MARK: - Global Mouse Event Handlers - func handleGlobalMouseDown(_ event: NSEvent) { - // Only start selection if multi-monitor selection mode is active and we're not already dragging - guard isMultiMonitorSelectionActive && !isDragging else { return } - - let globalLocation = NSEvent.mouseLocation - - print("โœ… Starting selection at: \(globalLocation)") - // Start drag selection at click location - startDragSelection(at: globalLocation) - } - - func handleGlobalMouseDragged(_ event: NSEvent) { - guard isMultiMonitorSelectionActive && isDragging else { return } - - let globalLocation = NSEvent.mouseLocation - - updateDragSelection(to: globalLocation) - } - - func handleGlobalMouseUp(_ event: NSEvent) { - guard isMultiMonitorSelectionActive && isDragging else { return } - - let globalLocation = NSEvent.mouseLocation - endDragSelection(at: globalLocation) - } - - // MARK: - Global Event Monitors - func setupGlobalKeyMonitor() { - // Remove existing monitor if any - removeGlobalKeyMonitor() - - // Add global key monitor for ESC key and custom modifier keys - globalKeyMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.keyDown, .keyUp, .flagsChanged]) { [weak self] event in - guard let self = self else { return } - - // Handle ESC key only during multi-monitor selection - if event.type == .keyDown && event.keyCode == 53 { // ESC key - if self.isMultiMonitorSelectionActive { - print("โŒจ๏ธ Global ESC key detected, canceling selection") - self.cancelMultiMonitorSelection() - } - return - } - - // Handle custom modifier keys for window capture mode toggle - ALWAYS active during screenshot mode - if event.type == .flagsChanged && self.isMultiMonitorSelectionActive { - let currentFlags = event.modifierFlags - let previousFlags = self.lastKnownModifierFlags - - // --- Command Toggle Logic (Uses lastKnownModifierFlags) --- - let commandFlag = NSEvent.ModifierFlags.command - if currentFlags.contains(commandFlag) && !previousFlags.contains(commandFlag) { - print("โŒจ๏ธ Command key JUST PRESSED (via flagsChanged) - Toggling All Screens Mode.") - self.toggleAllScreensCaptureMode() - } - // BELANGRIJK: De Command-toggle is nu onafhankelijk en zou de Option-logica niet moeten blokkeren. - - // --- Window Capture Logic removed (simplification) --- - // Window capture functionality is now handled through spacebar during drag selection - self.lastKnownModifierFlags = currentFlags // Update voor de volgende Command-toggle check - } - } - print("โŒจ๏ธ Global key monitor setup for ESC and custom modifier keys") - - // Also setup global mouse monitor to intercept ALL mouse events - setupGlobalMouseMonitor() - } - - func setupGlobalMouseMonitor() { - // Remove existing monitor if any - removeGlobalMouseMonitor() - - // IMPORTANT: Don't setup mouse monitors if we're in window capture mode - // Let the WindowCaptureManager handle mouse events instead - if isWindowCaptureMode { - print("๐ŸŽฏ Skipping global mouse monitor setup - in window capture mode") - return - } - - // Add BOTH global and local monitors for maximum coverage - let globalMonitor = NSEvent.addGlobalMonitorForEvents(matching: [ - .leftMouseDown, .leftMouseUp, .leftMouseDragged, - .rightMouseDown, .rightMouseUp, .rightMouseDragged, - .otherMouseDown, .otherMouseUp, .otherMouseDragged, - .mouseMoved, .mouseEntered, .mouseExited, .scrollWheel - ]) { [weak self] event in - guard let self = self, self.isMultiMonitorSelectionActive else { return } - - // IMPORTANT: Don't handle mouse events if in window capture mode - if self.isWindowCaptureMode { - return - } - - print("๐ŸŽฏ Global monitor intercepted: \(event.type.rawValue)") - // During selection, intercept and handle all mouse events ourselves - let globalLocation = NSEvent.mouseLocation - - switch event.type { - case .leftMouseDown: - if !self.isDragging { - // Check if Command key is pressed for all-screen capture - let allScreenModifier: UInt = (1 << 0) // Command key - self.isAllScreenModifierPressed = self.isModifierPressed(event.modifierFlags, modifier: allScreenModifier) - - self.mouseDownLocation = globalLocation - self.mouseDownTime = CACurrentMediaTime() - self.hasMouseMoved = false - - let modifierName = self.getModifierName(allScreenModifier) - print("๐ŸŽฏ Global mouse monitor - mouse down at: \(globalLocation), \(modifierName): \(self.isAllScreenModifierPressed)") - - // If all-screen modifier+click and setting is enabled, capture all screens immediately - if self.isAllScreenModifierPressed { - print("๐ŸŽฏ \(modifierName)+click detected - capturing all screens") - self.deactivateMultiMonitorSelection() - self.captureAllScreens() - self.resetTrackingVariables() // RESET for clean state - return - } - - // Otherwise start potential drag selection - self.startDragSelection(at: globalLocation) - } - case .leftMouseDragged: - if self.isDragging { - self.hasMouseMoved = true - self.updateDragSelection(to: globalLocation) - } - case .leftMouseUp: - if self.isDragging { - let timeSinceMouseDown = CACurrentMediaTime() - self.mouseDownTime - let distanceMoved = sqrt(pow(globalLocation.x - self.mouseDownLocation.x, 2) + pow(globalLocation.y - self.mouseDownLocation.y, 2)) - - print("๐ŸŽฏ Global mouse monitor - mouse up at: \(globalLocation)") - print("๐ŸŽฏ Time since down: \(timeSinceMouseDown)s, distance: \(distanceMoved)px, moved: \(self.hasMouseMoved)") - - // If it was a single click (no significant movement and quick) - if !self.hasMouseMoved && distanceMoved < 5.0 && timeSinceMouseDown < 0.5 && !self.isAllScreenModifierPressed { - // IMPORTANT: Don't capture screen if we're in window capture mode - if self.isWindowCaptureMode { - print("๐ŸŽฏ Single click in window capture mode - ignoring screen capture") - return // Global monitors return Void - } - - print("๐ŸŽฏ Single click detected - capturing current screen") - self.deactivateMultiMonitorSelection() - self.captureCurrentScreen(at: globalLocation) - self.resetTrackingVariables() // RESET for clean state - return // Global monitors return Void - } - - // Otherwise end drag selection normally - print("๐ŸŽฏ Global mouse monitor - ending drag selection at: \(globalLocation)") - self.endDragSelection(at: globalLocation) - } - // Global monitors return Void - case .rightMouseDown, .rightMouseUp: - print("๐ŸŽฏ Global mouse monitor - right-click, canceling selection") - self.cancelMultiMonitorSelection() - default: - // For all other mouse events during selection, just consume them - break - } - } - - // Add LOCAL monitor to intercept events within our app and BLOCK them from propagating - let localMonitor = NSEvent.addLocalMonitorForEvents(matching: [ - .leftMouseDown, .leftMouseUp, .leftMouseDragged, - .rightMouseDown, .rightMouseUp, .rightMouseDragged, - .otherMouseDown, .otherMouseUp, .otherMouseDragged, - .mouseMoved, .mouseEntered, .mouseExited, .scrollWheel - ]) { [weak self] event in - guard let self = self, self.isMultiMonitorSelectionActive else { return event } - - // Only print for non-mouseMoved events to reduce spam - if event.type != .mouseMoved { - print("๐ŸŽฏ Local monitor intercepted: \(event.type.rawValue)") - } - // During selection, handle the event ourselves and RETURN NIL to block propagation - let globalLocation = NSEvent.mouseLocation - - switch event.type { - case .leftMouseDown: - if !self.isDragging { - // Check if Command key is pressed for all-screen capture - let allScreenModifier: UInt = (1 << 0) // Command key - self.isAllScreenModifierPressed = self.isModifierPressed(event.modifierFlags, modifier: allScreenModifier) - - self.mouseDownLocation = globalLocation - self.mouseDownTime = CACurrentMediaTime() - self.hasMouseMoved = false - - let modifierName = self.getModifierName(allScreenModifier) - print("๐ŸŽฏ Local mouse monitor - mouse down at: \(globalLocation), \(modifierName): \(self.isAllScreenModifierPressed)") - - // If all-screen modifier+click and setting is enabled, capture all screens immediately - if self.isAllScreenModifierPressed { - print("๐ŸŽฏ \(modifierName)+click detected - capturing all screens") - self.deactivateMultiMonitorSelection() - self.captureAllScreens() - self.resetTrackingVariables() // RESET for clean state - return nil - } - - // Otherwise start potential drag selection - self.startDragSelection(at: globalLocation) - } - return nil // BLOCK the event from propagating - case .leftMouseDragged: - if self.isDragging { - self.hasMouseMoved = true - self.updateDragSelection(to: globalLocation) - } - return nil // BLOCK the event from propagating - case .leftMouseUp: - if self.isDragging { - let timeSinceMouseDown = CACurrentMediaTime() - self.mouseDownTime - let distanceMoved = sqrt(pow(globalLocation.x - self.mouseDownLocation.x, 2) + pow(globalLocation.y - self.mouseDownLocation.y, 2)) - - print("๐ŸŽฏ Local mouse monitor - mouse up at: \(globalLocation)") - print("๐ŸŽฏ Time since down: \(timeSinceMouseDown)s, distance: \(distanceMoved)px, moved: \(self.hasMouseMoved)") - - // If it was a single click (no significant movement and quick) - if !self.hasMouseMoved && distanceMoved < 5.0 && timeSinceMouseDown < 0.5 && !self.isAllScreenModifierPressed { - // IMPORTANT: Don't capture screen if we're in window capture mode - if self.isWindowCaptureMode { - print("๐ŸŽฏ Single click in window capture mode - ignoring screen capture") - return event // Let event continue in window capture mode - } - - print("๐ŸŽฏ Single click detected - capturing current screen") - self.deactivateMultiMonitorSelection() - self.captureCurrentScreen(at: globalLocation) - self.resetTrackingVariables() // RESET for clean state - return event // Let event pass through after reset - } - - // Otherwise end drag selection normally - print("๐ŸŽฏ Local mouse monitor - ending drag selection at: \(globalLocation)") - self.endDragSelection(at: globalLocation) - } - return nil // BLOCK the event from propagating - case .rightMouseDown, .rightMouseUp: - print("๐ŸŽฏ Local mouse monitor - right-click, canceling selection") - self.cancelMultiMonitorSelection() - return nil // BLOCK the event from propagating - case .mouseMoved: - // ๐Ÿ”ง FIX: Let mouseMoved events pass through for crosshair tracking - return event - default: - // For all other mouse events during selection, BLOCK them - return nil - } - } - - // Store both monitors - globalMouseMonitor = [globalMonitor as Any, localMonitor as Any] - print("๐ŸŽฏ Both global and local mouse monitors setup to intercept all mouse events") - } - - func removeGlobalKeyMonitor() { - if let monitor = globalKeyMonitor { - NSEvent.removeMonitor(monitor) - globalKeyMonitor = nil - print("โŒจ๏ธ Global key monitor removed") - } - removeGlobalMouseMonitor() - } - - func removeGlobalMouseMonitor() { - if let monitors = globalMouseMonitor { - if let monitorArray = monitors as? [Any] { - // Multiple monitors stored as array - for monitor in monitorArray { - NSEvent.removeMonitor(monitor) - } - print("๐ŸŽฏ Both global and local mouse monitors removed") - } else { - // Single monitor (fallback) - NSEvent.removeMonitor(monitors) - print("๐ŸŽฏ Single mouse monitor removed") - } - globalMouseMonitor = nil - } - } - - // MARK: - Custom Crosshair Overlay System - func hideCursor() { - // Try to hide cursor using CGDisplayHideCursor (may require private API) - // Alternative: use invisible cursor - let invisibleCursor = NSCursor() - invisibleCursor.set() - print("๐Ÿ™ˆ System cursor hidden") - } - - func showCursor() { - // Restore normal cursor - NSCursor.arrow.set() - print("๐Ÿ‘๏ธ System cursor restored") - } - - func setupCustomCrosshairOverlay() { - // Remove existing crosshair windows - removeCustomCrosshairOverlay() - - // Create crosshair windows for each screen - for screen in NSScreen.screens { - let crosshairWindow = createCrosshairWindow(for: screen) - crosshairWindows.append(crosshairWindow) - } - - // Setup mouse tracking to update crosshair position - crosshairTrackingMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged, .otherMouseDragged]) { [weak self] event in - self?.updateCrosshairPosition() - } - - // Also add local monitor for events within our app - let localMonitor = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved, .leftMouseDragged, .rightMouseDragged, .otherMouseDragged]) { [weak self] event in - self?.updateCrosshairPosition() - return event - } - - if let localMonitor = localMonitor { - // Store both monitors (we'll need to clean up both) - crosshairTrackingMonitor = [crosshairTrackingMonitor as Any, localMonitor] - } - - // IMPORTANT: Set initial crosshair position to current mouse location - updateCrosshairPosition() - - print("๐ŸŽฏ Custom crosshair overlay setup for all screens") - } - - func createCrosshairWindow(for screen: NSScreen) -> NSWindow { - let window = NSWindow( - contentRect: screen.frame, - styleMask: [.borderless], - backing: .buffered, - defer: false - ) - - window.backgroundColor = NSColor.clear - window.isOpaque = false - window.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.screenSaverWindow)) + 2) - window.ignoresMouseEvents = true - window.collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle] - - // Create crosshair view - let crosshairView = CrosshairView(frame: window.contentView!.bounds) - window.contentView = crosshairView - - window.orderFront(nil) - return window - } - - func updateCrosshairPosition() { - guard isMultiMonitorSelectionActive else { return } - - let mouseLocation = NSEvent.mouseLocation - - for window in crosshairWindows { - if let crosshairView = window.contentView as? CrosshairView { - // Convert global mouse location to window coordinates - let windowLocation = window.convertPoint(fromScreen: mouseLocation) - crosshairView.updateCrosshairPosition(windowLocation) - } - } - } - - func removeCustomCrosshairOverlay() { - // Remove all crosshair windows - for window in crosshairWindows { - window.orderOut(nil) - } - crosshairWindows.removeAll() - - // Remove mouse tracking monitor - if let monitor = crosshairTrackingMonitor { - if let monitorArray = monitor as? [Any] { - // Multiple monitors stored as array - for mon in monitorArray { - NSEvent.removeMonitor(mon) - } - } else { - // Single monitor - NSEvent.removeMonitor(monitor) - } - crosshairTrackingMonitor = nil - } - - print("๐ŸŽฏ Custom crosshair overlay removed") - } - - // MARK: - Multi-Monitor Screenshot Selection Logic - func startDragSelection(at point: NSPoint) { - isDragging = true - startPoint = point - currentEndPoint = point - - print("๐Ÿ–ฑ๏ธ Start multi-monitor drag selection at: \(point)") - - // Create initial overlay windows - createOverlayWindows(from: startPoint, to: currentEndPoint) - } - - func updateDragSelection(to point: NSPoint) { - guard isDragging else { return } - - // Throttle updates for performance - let currentTime = CACurrentMediaTime() - guard currentTime - lastUpdateTime >= updateThrottle else { return } - lastUpdateTime = currentTime - - currentEndPoint = point - - // Update overlay windows to show current selection - updateOverlayWindows(from: startPoint, to: currentEndPoint) - } - - func endDragSelection(at point: NSPoint) { - guard isDragging else { return } - - isDragging = false - currentEndPoint = point - - print("๐Ÿ–ฑ๏ธ End multi-monitor drag selection at: \(point)") - - // Calculate final selection rectangle - let selectionRect = calculateSelectionRect(from: startPoint, to: currentEndPoint) - - // Hide overlay windows - hideMultiMonitorOverlayWindows() - - // Deactivate selection mode - deactivateMultiMonitorSelection() - - // Only capture if selection is meaningful (at least 2x2 pixels) - if selectionRect.width >= 2 && selectionRect.height >= 2 { - print("๐Ÿ“ธ Capturing multi-monitor screenshot of area: \(selectionRect)") - captureMultiMonitorScreenshot(in: selectionRect) - } else { - print("โŒ Selection too small, canceling multi-monitor screenshot") - } - } - - func cancelMultiMonitorSelection() { - isDragging = false - isMultiMonitorSelectionActive = false - isWindowCaptureMode = false // Reset window capture mode - - // Hide overlay windows - hideMultiMonitorOverlayWindows() - - // Remove event capture window - removeEventCaptureWindow() - - // Remove global key monitor - removeGlobalKeyMonitor() - - // Remove custom crosshair overlay - removeCustomCrosshairOverlay() - - // Show cursor again - showCursor() - - // Reset tracking variables for clean state - resetTrackingVariables() - - // Deactivate window capture if active - if #available(macOS 12.3, *) { - windowCaptureManager?.deactivateWindowSelectionMode() - } - - print("โŒ Multi-monitor selection canceled") - } - - func deactivateMultiMonitorSelection() { - isMultiMonitorSelectionActive = false - isWindowCaptureMode = false // Reset window capture mode - - // Remove event capture window - removeEventCaptureWindow() - - // Remove global key monitor - removeGlobalKeyMonitor() - - // Remove custom crosshair overlay - removeCustomCrosshairOverlay() - - // Show cursor again - showCursor() - - // Reset tracking variables for clean state - resetTrackingVariables() - - // Deactivate window capture if active - if #available(macOS 12.3, *) { - windowCaptureManager?.deactivateWindowSelectionMode() - } - - print("๐Ÿ”„ Multi-monitor selection mode deactivated") - } - - func showSelectionModeNotification() { - // Create a temporary notification window - let notificationWindow = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 300, height: 80), - styleMask: [.borderless], - backing: .buffered, - defer: false - ) - - notificationWindow.backgroundColor = NSColor.clear - notificationWindow.isOpaque = false - notificationWindow.level = .floating - notificationWindow.ignoresMouseEvents = true - - // Create notification view - let notificationView = NSView(frame: notificationWindow.frame) - notificationView.wantsLayer = true - - // Background with blur effect - let blurView = NSVisualEffectView(frame: notificationView.bounds) - blurView.blendingMode = NSVisualEffectView.BlendingMode.behindWindow - blurView.material = NSVisualEffectView.Material.hudWindow - blurView.state = NSVisualEffectView.State.active - blurView.layer?.cornerRadius = 12 - blurView.layer?.masksToBounds = true - notificationView.addSubview(blurView) - - // Text label - let label = NSTextField(labelWithString: "๐Ÿ“ธ Multi-Monitor Screenshot Mode\nClick on empty desktop space to start") - label.font = NSFont.systemFont(ofSize: 13, weight: .medium) - label.textColor = .white - label.alignment = .center - label.frame = NSRect(x: 20, y: 20, width: 260, height: 40) - notificationView.addSubview(label) - - notificationWindow.contentView = notificationView - - // Position at top center of main screen - if let mainScreen = NSScreen.main { - let screenFrame = mainScreen.frame - let x = screenFrame.midX - notificationWindow.frame.width / 2 - let y = screenFrame.maxY - 100 - notificationWindow.setFrameOrigin(NSPoint(x: x, y: y)) - } - - // Show with animation - notificationWindow.alphaValue = 0 - notificationWindow.orderFront(nil) - - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.3 - notificationWindow.animator().alphaValue = 1.0 - } - - // Auto-hide after 3 seconds - DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.3 - notificationWindow.animator().alphaValue = 0 - }) { - notificationWindow.orderOut(nil as Any?) - } - } - } - - // MARK: - Multi-Window Overlay System - func createOverlayWindows(from start: NSPoint, to end: NSPoint) { - let selectionRect = calculateSelectionRect(from: start, to: end) - - // Clear any existing windows - hideMultiMonitorOverlayWindows() - - // Get all screens that the selection intersects - let intersectingScreens = NSScreen.screens.filter { screen in - selectionRect.intersects(screen.frame) - } - - // Create a separate window for each intersecting screen - for screen in intersectingScreens { - let intersection = selectionRect.intersection(screen.frame) - - // Only create window if intersection is meaningful - if intersection.width > 1 && intersection.height > 1 { - createSingleOverlayWindow(for: intersection, on: screen) - } - } - } - - func createSingleOverlayWindow(for rect: NSRect, on screen: NSScreen) { - let window = NSPanel( - contentRect: rect, - styleMask: [.borderless, .nonactivatingPanel], - backing: .buffered, - defer: false - ) - - // Configure window properties for multi-monitor support - window.level = .screenSaver // Below event capture window but above everything else - window.isOpaque = false - window.backgroundColor = .clear - window.hasShadow = false - window.ignoresMouseEvents = true // Don't interfere with mouse tracking - window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .ignoresCycle, .stationary] - - // Disable automatic positioning - window.setFrameAutosaveName("") - window.isMovableByWindowBackground = false - window.isMovable = false - window.animationBehavior = .none - window.displaysWhenScreenProfileChanges = true - - // Panel-specific settings - let panel = window - panel.isFloatingPanel = true - panel.becomesKeyOnlyIfNeeded = false - panel.hidesOnDeactivate = false - - // Create and set content view with multi-monitor selection overlay - let contentView = MultiMonitorSelectionOverlayView() - let hostingView = NSHostingView(rootView: contentView) - - // Create a container view that will hold both the SwiftUI view and the crosshair - let containerView = NSView(frame: rect) - containerView.addSubview(hostingView) - hostingView.frame = containerView.bounds - hostingView.autoresizingMask = [.width, .height] - - // Add crosshair view on top - let crosshairView = CrosshairCursorView(frame: NSRect(x: 0, y: 0, width: 24, height: 24)) - containerView.addSubview(crosshairView) - crosshairView.startTracking() - - window.contentView = containerView - - // Set frame and show - window.setFrame(rect, display: false, animate: false) - window.makeKeyAndOrderFront(nil as Any?) - - // Add to our collection - overlayWindows.append(window) - windowScreenMap[window] = screen - - print("๐ŸชŸ Created multi-monitor overlay window on \(screen.localizedName) at \(rect)") - } - - func updateOverlayWindows(from start: NSPoint, to end: NSPoint) { - let selectionRect = calculateSelectionRect(from: start, to: end) - - // Get all screens that the selection intersects - let intersectingScreens = NSScreen.screens.filter { screen in - selectionRect.intersects(screen.frame) - } - - // Smart update: reuse existing windows when possible - updateExistingWindows(for: intersectingScreens, selectionRect: selectionRect) - } - - func updateExistingWindows(for screens: [NSScreen], selectionRect: NSRect) { - // Track which screens we've updated - var updatedScreens: Set = [] - - // Update existing windows that are still relevant - overlayWindows = overlayWindows.compactMap { window in - guard let associatedScreen = windowScreenMap[window] else { - window.close() - return nil - } - - // Check if this screen still intersects with the selection - let intersection = selectionRect.intersection(associatedScreen.frame) - if intersection.width > 1 && intersection.height > 1 { - // Update the existing window smoothly - updateWindowFrame(window, to: intersection) - updatedScreens.insert(associatedScreen) - return window - } else { - // This screen no longer intersects, remove the window - window.close() - windowScreenMap.removeValue(forKey: window) - return nil - } - } - - // Create new windows for screens that don't have one yet - for screen in screens { - if !updatedScreens.contains(screen) { - let intersection = selectionRect.intersection(screen.frame) - if intersection.width > 1 && intersection.height > 1 { - createSingleOverlayWindow(for: intersection, on: screen) - } - } - } - } - - func updateWindowFrame(_ window: NSWindow, to rect: NSRect) { - // Fast frame update without recreation - window.setFrame(rect, display: true, animate: false) - } - - func hideMultiMonitorOverlayWindows() { - overlayWindows.forEach { $0.close() } - overlayWindows.removeAll() - windowScreenMap.removeAll() - print("๐ŸชŸ All multi-monitor overlay windows hidden") - } - - // MARK: - Screen Mapping and Calculation - func calculateSelectionRect(from start: NSPoint, to end: NSPoint) -> NSRect { - let minX = min(start.x, end.x) - let maxX = max(start.x, end.x) - let minY = min(start.y, end.y) - let maxY = max(start.y, end.y) - - return NSRect( - x: minX, - y: minY, - width: maxX - minX, - height: maxY - minY - ) - } - - func findWindowForScreen(_ screen: NSScreen) -> NSWindow? { - return overlayWindows.first { window in - windowScreenMap[window] == screen - } - } - - // MARK: - Multi-Monitor Screenshot Capture - func captureMultiMonitorScreenshot(in rect: NSRect) { - print("๐Ÿ“ธ Capturing multi-monitor screenshot...") - print("๐Ÿ“ Multi-monitor capture area: x=\(rect.origin.x), y=\(rect.origin.y), width=\(rect.width), height=\(rect.height)") - - // Convert from global screen coordinates to proper capture coordinates - let convertedRect = convertGlobalToScreenCoordinates(rect) - print("๐ŸŽฏ Converted capture area: x=\(convertedRect.origin.x), y=\(convertedRect.origin.y), width=\(convertedRect.width), height=\(convertedRect.height)") - - // Use the existing capture method with converted coordinates and multi-monitor flag - capture(rect: convertedRect, isMultiMonitor: true) - } - - func convertGlobalToScreenCoordinates(_ globalRect: NSRect) -> NSRect { - // NSEvent.mouseLocation and CGWindowListCreateImage both use the same coordinate system: - // - (0,0) is at the bottom-left of the main screen - // - Y increases upward - // - No conversion needed! - - print("๐Ÿ”„ Coordinate conversion (SIMPLIFIED):") - print(" Input rect: \(globalRect)") - print(" Output rect: \(globalRect) (NO CONVERSION)") - - // Diagnostic: Show which screens this rect intersects - let intersectingScreens = NSScreen.screens.filter { screen in - globalRect.intersects(screen.frame) - } - - print(" ๐Ÿ“บ Intersecting screens:") - for screen in intersectingScreens { - let intersection = globalRect.intersection(screen.frame) - print(" - \(screen.localizedName): intersection \(intersection)") - } - - return globalRect - } - - // MARK: - Screen Information - func listAvailableScreens() { - print("๐Ÿ“บ Available screens for multi-monitor support:") - - let screens = NSScreen.screens - let mainScreen = NSScreen.main - - for (index, screen) in screens.enumerated() { - let frame = screen.frame - let isMain = screen == mainScreen - let _ = screen.deviceDescription - let displayName = screen.localizedName - - print(" \(index): \(displayName)") - print(" Frame: x=\(frame.origin.x), y=\(frame.origin.y), w=\(frame.width), h=\(frame.height)") - print(" Scale: \(screen.backingScaleFactor)x") - - if isMain { - print(" โญ Main screen (primary display)") - } - - // Check for potential issues - if frame.origin.x != 0 && frame.origin.y != 0 { - print(" ๐Ÿ“ Positioned screen (not at origin)") - } - - // Show relative position - if frame.origin.y > 0 { - print(" โฌ†๏ธ Screen positioned ABOVE main screen") - } else if frame.origin.y < 0 { - print(" โฌ‡๏ธ Screen positioned BELOW main screen") - } - - if frame.origin.x > 0 { - print(" โžก๏ธ Screen positioned RIGHT of main screen") - } else if frame.origin.x < 0 { - print(" โฌ…๏ธ Screen positioned LEFT of main screen") - } - } - - // Calculate and display total desktop bounds - let totalBounds = getAllScreensBounds() - print("๐ŸŒ Total desktop bounds: x=\(totalBounds.origin.x) to \(totalBounds.maxX), y=\(totalBounds.origin.y) to \(totalBounds.maxY)") - print("๐Ÿ“ Total desktop size: \(totalBounds.width) ร— \(totalBounds.height)") - - // Show coordinate system info - print("๐Ÿงญ macOS Coordinate System Info:") - print(" - (0,0) is at bottom-left of main screen") - print(" - Y increases upward") - print(" - Screens above main have positive Y") - print(" - Screens below main have negative Y") - - // App Store compatibility check - if screens.count > 2 { - print("๐Ÿ” Multi-screen setup detected (\(screens.count) screens) - testing compatibility") - } - - // Check for unusual configurations - let hasNegativeCoordinates = screens.contains { $0.frame.origin.x < 0 || $0.frame.origin.y < 0 } - if hasNegativeCoordinates { - print("โš ๏ธ Negative coordinates detected - using advanced coordinate conversion") - } - } - - // MARK: - Cleanup - func cleanupMultiMonitorResources() { - // Remove all event monitors - if let monitor = globalMouseDownMonitor { - NSEvent.removeMonitor(monitor) - globalMouseDownMonitor = nil - } - if let monitor = globalMouseDragMonitor { - NSEvent.removeMonitor(monitor) - globalMouseDragMonitor = nil - } - if let monitor = globalMouseUpMonitor { - NSEvent.removeMonitor(monitor) - globalMouseUpMonitor = nil - } - if let monitor = localMouseDownMonitor { - NSEvent.removeMonitor(monitor) - localMouseDownMonitor = nil - } - if let monitor = localMouseDragMonitor { - NSEvent.removeMonitor(monitor) - localMouseDragMonitor = nil - } - if let monitor = localMouseUpMonitor { - NSEvent.removeMonitor(monitor) - localMouseUpMonitor = nil - } - - // Clean up windows - hideMultiMonitorOverlayWindows() - - // Remove global key monitor - removeGlobalKeyMonitor() - - // Remove custom crosshair overlay - removeCustomCrosshairOverlay() - - print("๐Ÿงน Multi-monitor resources cleaned up") - } - - // MARK: - Multi-Monitor Event Capture Window Management - func createEventCaptureWindow() { - // Remove any existing capture windows - removeEventCaptureWindow() - - // Create a separate event capture window for each screen - for (index, screen) in NSScreen.screens.enumerated() { - let captureWindow = EventCaptureWindow( - contentRect: screen.frame, - styleMask: [.borderless], - backing: .buffered, - defer: false - ) - - // Configure the window to be invisible but capture ALL events - captureWindow.backgroundColor = NSColor.clear - captureWindow.isOpaque = false - captureWindow.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.assistiveTechHighWindow)) + 10) // Even higher level - captureWindow.ignoresMouseEvents = false - captureWindow.acceptsMouseMovedEvents = true - captureWindow.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .ignoresCycle, .stationary] - - // CRITICAL: Prevent other windows from receiving events - captureWindow.hidesOnDeactivate = false - captureWindow.isMovableByWindowBackground = false - - // Force the window to stay on top and capture all events - captureWindow.sharingType = .none - captureWindow.isExcludedFromWindowsMenu = true - - // Create a container view for both event capture and crosshair - let containerView = NSView(frame: NSRect(origin: .zero, size: screen.frame.size)) - - // Create a custom view that handles mouse events - let captureView = EventCaptureView() - captureView.screenshotApp = self - captureView.frame = containerView.bounds - containerView.addSubview(captureView) - - // Add crosshair view on top - let crosshairView = CrosshairCursorView(frame: NSRect(x: 0, y: 0, width: 24, height: 24)) - containerView.addSubview(crosshairView) - crosshairView.startTracking() - - captureWindow.contentView = containerView - - // Position the window exactly on this screen - captureWindow.setFrame(screen.frame, display: false) - - // Make sure this window is always on top - captureWindow.orderFrontRegardless() - - // Store the window - eventCaptureWindows.append(captureWindow) - - print("๐ŸŽฏ Event capture window \(index) created for screen: \(screen.localizedName) at \(screen.frame)") - } - - // Make the first window key to receive keyboard events - if let firstWindow = eventCaptureWindows.first { - firstWindow.makeKey() - - // CRITICAL: Force our app to become active and prevent other apps from receiving events - NSApp.activate(ignoringOtherApps: true) - firstWindow.makeKeyAndOrderFront(nil as Any?) - - // Make the capture view the first responder for keyboard events - DispatchQueue.main.async { - if let captureView = firstWindow.contentView?.subviews.first(where: { $0 is EventCaptureView }) { - firstWindow.makeFirstResponder(captureView) - print("โŒจ๏ธ Made EventCaptureView first responder on primary capture window") - } - - // Additional activation to ensure we're really on top - NSApp.activate(ignoringOtherApps: true) - } - } - - print("๐Ÿ”’ Created \(eventCaptureWindows.count) event capture windows for multi-monitor setup") - print("๐Ÿ”’ App activated to capture all events") - } - - func removeEventCaptureWindow() { - for window in eventCaptureWindows { - window.orderOut(nil as Any?) - } - eventCaptureWindows.removeAll() - - // Also remove the legacy single window if it exists - if let captureWindow = eventCaptureWindow { - captureWindow.orderOut(nil as Any?) - eventCaptureWindow = nil - } - - print("๐Ÿ—‘๏ธ All event capture windows removed") - } - - func getAllScreensBounds() -> NSRect { - guard !NSScreen.screens.isEmpty else { - return NSRect(x: 0, y: 0, width: 1920, height: 1080) // Fallback - } - - print("๐Ÿ–ฅ๏ธ Detecting screens for event capture:") - for (index, screen) in NSScreen.screens.enumerated() { - print(" Screen \(index): \(screen.localizedName) - Frame: \(screen.frame)") - } - - var minX: CGFloat = CGFloat.greatestFiniteMagnitude - var minY: CGFloat = CGFloat.greatestFiniteMagnitude - var maxX: CGFloat = -CGFloat.greatestFiniteMagnitude - var maxY: CGFloat = -CGFloat.greatestFiniteMagnitude - - for screen in NSScreen.screens { - let frame = screen.frame - minX = min(minX, frame.minX) - minY = min(minY, frame.minY) - maxX = max(maxX, frame.maxX) - maxY = max(maxY, frame.maxY) - } - - let combinedBounds = NSRect(x: minX, y: minY, width: maxX - minX, height: maxY - minY) - print("๐ŸŽฏ Combined screen bounds for event capture: \(combinedBounds)") - - return combinedBounds - } - - // MARK: - Window Capture Mode State - var isWindowCaptureMode: Bool { - get { - return objc_getAssociatedObject(self, &ScreenshotApp.isWindowCaptureModeKey) as? Bool ?? false - } - set { - objc_setAssociatedObject(self, &ScreenshotApp.isWindowCaptureModeKey, newValue, .OBJC_ASSOCIATION_ASSIGN) - } - } - - private static var isWindowCaptureModeKey: UInt8 = 0 - - // MARK: - Window Capture Mode Switching - func switchToWindowCaptureMode() { - guard !isWindowCaptureMode else { return } - - print("๐ŸชŸ Switching to window capture mode") - isWindowCaptureMode = true - - // IMPORTANT: Remove normal screenshot event capture windows - removeEventCaptureWindow() - removeCustomCrosshairOverlay() - - // CRITICAL: Also remove all mouse and key monitors that interfere with window capture - removeGlobalKeyMonitor() // This also calls removeGlobalMouseMonitor() - - // CRITICAL: Also clean up the local mouse tracking from setupGlobalMouseTracking() - if let monitor = localMouseDownMonitor { - NSEvent.removeMonitor(monitor) - localMouseDownMonitor = nil - } - if let monitor = localMouseDragMonitor { - NSEvent.removeMonitor(monitor) - localMouseDragMonitor = nil - } - if let monitor = localMouseUpMonitor { - NSEvent.removeMonitor(monitor) - localMouseUpMonitor = nil - } - if let monitor = globalMouseDownMonitor { - NSEvent.removeMonitor(monitor) - globalMouseDownMonitor = nil - } - if let monitor = globalMouseDragMonitor { - NSEvent.removeMonitor(monitor) - globalMouseDragMonitor = nil - } - if let monitor = globalMouseUpMonitor { - NSEvent.removeMonitor(monitor) - globalMouseUpMonitor = nil - } - print("๐ŸŽฏ All multi-monitor mouse tracking disabled for window capture mode") - - // Activate window capture functionality - if #available(macOS 12.3, *) { - windowCaptureManager?.activateWindowSelectionMode() - } else { - print("โš ๏ธ Window capture requires macOS 12.3 or later") - isWindowCaptureMode = false - // Restore normal screenshot mode if window capture fails - createEventCaptureWindow() - setupCustomCrosshairOverlay() - setupGlobalKeyMonitor() // Restore monitors - setupGlobalMouseTracking() // Restore local mouse tracking - } - } - - func switchToNormalScreenshotMode() { - guard isWindowCaptureMode else { return } - - print("๐Ÿ“ธ Switching back to normal screenshot mode") - isWindowCaptureMode = false - - // Deactivate window capture functionality - if #available(macOS 12.3, *) { - windowCaptureManager?.deactivateWindowSelectionMode() - } - - // IMPORTANT: Restore normal screenshot event capture and crosshair - createEventCaptureWindow() - setupCustomCrosshairOverlay() - - // CRITICAL: Restore the global key monitor for normal screenshot mode - setupGlobalKeyMonitor() - - // CRITICAL: Restore the local mouse tracking for normal screenshot mode - setupGlobalMouseTracking() - print("๐ŸŽฏ All multi-monitor mouse tracking restored for normal screenshot mode") - } - - // MARK: - Helper Functions for Custom Modifiers - func isModifierPressed(_ flags: NSEvent.ModifierFlags, modifier: UInt) -> Bool { - if modifier & (1 << 0) != 0 && !flags.contains(.command) { return false } - if modifier & (1 << 1) != 0 && !flags.contains(.shift) { return false } - if modifier & (1 << 2) != 0 && !flags.contains(.option) { return false } - if modifier & (1 << 3) != 0 && !flags.contains(.control) { return false } - - // Check that only the required modifiers are pressed - let requiredCommand = modifier & (1 << 0) != 0 - let requiredShift = modifier & (1 << 1) != 0 - let requiredOption = modifier & (1 << 2) != 0 - let requiredControl = modifier & (1 << 3) != 0 - - return flags.contains(.command) == requiredCommand && - flags.contains(.shift) == requiredShift && - flags.contains(.option) == requiredOption && - flags.contains(.control) == requiredControl - } - - func getModifierName(_ modifier: UInt) -> String { - var parts: [String] = [] - - if modifier & (1 << 3) != 0 { parts.append("Control") } - if modifier & (1 << 2) != 0 { parts.append("Option") } - if modifier & (1 << 1) != 0 { parts.append("Shift") } - if modifier & (1 << 0) != 0 { parts.append("Command") } - - return parts.joined(separator: "+") - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/OverlayComponents.swift b/ShotScreen/Sources/OverlayComponents.swift deleted file mode 100644 index 9dfa09b..0000000 --- a/ShotScreen/Sources/OverlayComponents.swift +++ /dev/null @@ -1,253 +0,0 @@ -import AppKit - -// MARK: - Overlay Window for Screenshot Selection -class OverlayWindow: NSWindow { - var crosshairView: CrosshairCursorView? - var onCancelRequested: (() -> Void)? - - init(screen: NSScreen) { - super.init(contentRect: screen.frame, - styleMask: [.borderless], - backing: .buffered, - defer: false) - self.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.mainMenuWindow)) + 1) - self.isOpaque = false - self.backgroundColor = NSColor.clear - self.ignoresMouseEvents = false - self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] - - // Try to hide the system cursor - CGDisplayHideCursor(CGMainDisplayID()) - - // Create an empty content view first - let contentContainer = NSView(frame: screen.frame) - contentContainer.wantsLayer = true - self.contentView = contentContainer - - // Make this window visible - self.makeKeyAndOrderFront(nil as Any?) - self.orderFrontRegardless() - - // Add our custom crosshair view at the top level - crosshairView = CrosshairCursorView(frame: NSRect(x: 0, y: 0, width: 24, height: 24)) - if let crosshairView = crosshairView { - contentContainer.addSubview(crosshairView) - - // Get current mouse position and update crosshair initial position - let mouseLoc = NSEvent.mouseLocation - let windowLoc = self.convertPoint(fromScreen: mouseLoc) - crosshairView.frame = NSRect(x: windowLoc.x - 12, y: windowLoc.y - 12, width: 24, height: 24) - - // Start tracking timer - crosshairView.startTracking() - } - } - - deinit { - // Show the cursor again when window is destroyed - CGDisplayShowCursor(CGMainDisplayID()) - } - - // Forceer dat het window key events kan ontvangen, zelfs zonder title bar - override var canBecomeKey: Bool { - return true - } - - override func keyDown(with event: NSEvent) { - if event.keyCode == 53 { // 53 is Escape - print("ESC key pressed in OverlayWindow - keyDown") - - // Roep alleen de callback aan - onCancelRequested?() - - // Sluit het venster NIET HIER - } else { - // Voor andere toetsen, stuur door naar super - super.keyDown(with: event) - } - } - - override func rightMouseDown(with event: NSEvent) { - print("๐Ÿ–ฑ Right mouse down in OverlayWindow") - // Roep alleen de callback aan - onCancelRequested?() - // Roep super niet aan, we handelen dit af. - } - - override func close() { - // Show the cursor before closing - CGDisplayShowCursor(CGMainDisplayID()) - super.close() - } -} - -// MARK: - Selection View for Drawing Selection Rectangle -class SelectionView: NSView { - var startPoint: NSPoint? - var endPoint: NSPoint? - var selectionLayer = CAShapeLayer() - var onSelectionComplete: ((NSRect) -> Void)? - weak var parentWindow: OverlayWindow? - private var isSelecting = false - private var hasStartedSelection = false - - override var acceptsFirstResponder: Bool { true } - - // Add mouse event monitoring - private var eventMonitor: Any? - - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - self.wantsLayer = true - - // Listen to mouse events at the application level - setupEventMonitor() - } - - private func setupEventMonitor() { - eventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .leftMouseDragged, .leftMouseUp]) { [weak self] event in - guard let self = self else { return event } - - // Get the location in window coordinates - let locationInWindow = event.locationInWindow - - switch event.type { - case .leftMouseDown: - self.handleMouseDown(at: locationInWindow) - case .leftMouseDragged: - self.handleMouseDragged(at: locationInWindow) - case .leftMouseUp: - self.handleMouseUp(at: locationInWindow) - default: - break - } - - return event - } - } - - // Maak internal ipv private - func removeEventMonitor() { - if let eventMonitor = eventMonitor { - NSEvent.removeMonitor(eventMonitor) - self.eventMonitor = nil - } - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - } - - override func viewDidMoveToWindow() { - super.viewDidMoveToWindow() - parentWindow = window as? OverlayWindow - } - - override func removeFromSuperview() { - // Remove event monitor - removeEventMonitor() - - // Show cursor when removed - CGDisplayShowCursor(CGMainDisplayID()) - super.removeFromSuperview() - } - - func handleMouseDown(at location: NSPoint) { - isSelecting = true - hasStartedSelection = true - startPoint = location - endPoint = location - updateSelectionLayer() - print("๐Ÿ–ฑ Mouse down at: \(location)") - - // Update crosshair position - if let parentWindow = parentWindow, - let crosshairView = parentWindow.crosshairView { - crosshairView.updatePosition(location: location) - } - } - - func handleMouseDragged(at location: NSPoint) { - guard isSelecting && hasStartedSelection else { return } - - endPoint = location - updateSelectionLayer() - - // Update crosshair position - if let parentWindow = parentWindow, - let crosshairView = parentWindow.crosshairView { - crosshairView.updatePosition(location: location) - } - } - - func handleMouseUp(at location: NSPoint) { - guard isSelecting && hasStartedSelection else { return } - - isSelecting = false - - // Set the end point - endPoint = location - - guard let start = startPoint, let end = endPoint else { return } - - // Calculate the selection rectangle in screen coordinates - let rect: NSRect - - // If start and end are very close (within 5 pixels), assume user wants the full screen - if abs(start.x - end.x) < 5 && abs(start.y - end.y) < 5 { - rect = self.bounds - print("๐Ÿ“บ Full screen capture requested") - } else { - rect = NSRect(x: min(start.x, end.x), - y: min(start.y, end.y), - width: abs(start.x - end.x), - height: abs(start.y - end.y)) - print("โœ‚๏ธ Selection completed: \(rect)") - } - - // Show cursor before completing - CGDisplayShowCursor(CGMainDisplayID()) - - // Remove the event monitor - removeEventMonitor() - - onSelectionComplete?(rect) - removeFromSuperview() - window?.orderOut(nil as Any?) - } - - // Keep these for direct interactions with the view - override func mouseDown(with event: NSEvent) { - handleMouseDown(at: event.locationInWindow) - } - - override func mouseDragged(with event: NSEvent) { - handleMouseDragged(at: event.locationInWindow) - } - - override func mouseUp(with event: NSEvent) { - handleMouseUp(at: event.locationInWindow) - } - - func updateSelectionLayer() { - selectionLayer.removeFromSuperlayer() - - // Only create selection if we have valid start and end points - guard let start = startPoint, let end = endPoint, - abs(start.x - end.x) >= 1 || abs(start.y - end.y) >= 1 else { - return - } - - let rect = NSRect(x: min(start.x, end.x), - y: min(start.y, end.y), - width: max(1, abs(start.x - end.x)), - height: max(1, abs(start.y - end.y))) - - let path = CGPath(rect: rect, transform: nil) - selectionLayer.path = path - selectionLayer.fillColor = NSColor(calibratedWhite: 1, alpha: 0.3).cgColor - selectionLayer.strokeColor = NSColor.white.cgColor - selectionLayer.lineWidth = 1.0 - self.layer?.addSublayer(selectionLayer) - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/PreviewManager.swift b/ShotScreen/Sources/PreviewManager.swift deleted file mode 100644 index 72e9c87..0000000 --- a/ShotScreen/Sources/PreviewManager.swift +++ /dev/null @@ -1,1804 +0,0 @@ -import AppKit -import SwiftUI - - // MARK: - Button Hover Handler (glass-effect safe) -class ButtonHoverHandler: NSObject { - weak var button: NSButton? - private let zoomScale: CGFloat - - init(button: NSButton, zoomScale: CGFloat = 1.3) { - self.button = button - self.zoomScale = zoomScale - super.init() - } - - func mouseEntered(with event: NSEvent) { - guard let button = button else { return } - - print("๐ŸŽฏ HOVER: Mouse entered button - starting zoom (\(zoomScale)x) and color effect") - - let hoverColor = ThemeManager.shared.buttonHoverColor // Adaptive hover color - - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.2 - context.timingFunction = CAMediaTimingFunction(name: .easeOut) - context.allowsImplicitAnimation = true - - // Color change - button.animator().contentTintColor = hoverColor - }) - - // Zoom effect using layer transform (separate from color animation) - if let layer = button.layer { - CATransaction.begin() - CATransaction.setAnimationDuration(0.2) - CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .easeOut)) - let scaleTransform = CATransform3DMakeScale(zoomScale, zoomScale, 1.0) - layer.transform = scaleTransform - CATransaction.commit() - print("๐ŸŽฏ HOVER: Applied \(zoomScale)x zoom scale") - } - } - - func mouseExited(with event: NSEvent) { - guard let button = button else { return } - - print("๐ŸŽฏ HOVER: Mouse exited button - restoring original size and color") - - // Get original color - let originalColor = objc_getAssociatedObject(button, "originalColor") as? NSColor ?? ThemeManager.shared.buttonOriginalColor - - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.25 - context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) - context.allowsImplicitAnimation = true - - // Restore original color - button.animator().contentTintColor = originalColor - }) - - // Restore original size (separate from color animation) - if let layer = button.layer { - CATransaction.begin() - CATransaction.setAnimationDuration(0.25) - CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .easeInEaseOut)) - layer.transform = CATransform3DIdentity - CATransaction.commit() - print("๐ŸŽฏ HOVER: Restored original scale") - } - } -} - -// MARK: - Preview Window Management -class PreviewManager: NSObject { - weak var delegate: PreviewManagerDelegate? - - // MARK: - Properties - var activePreviewWindow: NSWindow? - var currentImageView: DraggableImageView? - var previewDismissTimer: Timer? - var isClosing = false - var isPreviewUpdating = false - - // NIEUW: Loading indicator properties - var isShowingLoadingIndicator = false - var loadingOverlayWindow: NSWindow? - - // ๐ŸŽจ NEW: Background Removal Mode Properties - var isBackgroundRemovalMode = false - var isBGRTransition = false // ๐Ÿ”„ NEW: Flag to prevent BGR reset during BGR actions - var originalImage: NSImage? - var processedImage: NSImage? - var bgrResetButton: BGROverlayButton? - var bgrToggleButton: BGROverlayButton? - - // ๐ŸŽจ NEW: BGR File Management Properties - var originalImageURL: URL? // File URL for original image - var processedImageURL: URL? // File URL for processed image - var isShowingProcessedImage = false // Track which image is currently displayed - - // ๐ŸŽจ NEW: BGR Progress Bar Properties - var bgrProgressContainer: NSView? - var bgrProgressBar: NSProgressIndicator? - var bgrProgressLabel: NSTextField? - - // MARK: - ๐ŸŽ› TOOLBAR BUTTON AANPASSINGEN - - // ๐Ÿ“ BUTTON GROOTTES - let closeButtonSize: CGFloat = 9 // ๐Ÿ”ด Close button (X) - Klein - let saveButtonSize: CGFloat = 16 // ๐Ÿ’พ Save button (Map+Plus) - let folderButtonSize: CGFloat = 16 // ๐Ÿ“ Folder button (Map) - let settingsButtonSize: CGFloat = 14 // โš™๏ธ Settings button (Tandwiel) - - // ๐Ÿ“ BUTTON VERTICALE POSITIES (offset van center) - let closeButtonVerticalOffset: CGFloat = -0.5 // ๐Ÿ”ด Close button verticaal - let saveButtonVerticalOffset: CGFloat = 0 // ๐Ÿ’พ Save button verticaal - let folderButtonVerticalOffset: CGFloat = -0.5 // ๐Ÿ“ Folder button verticaal - let settingsButtonVerticalOffset: CGFloat = 0 // โš™๏ธ Settings button verticaal - - // โ†”๏ธ BUTTON HORIZONTALE POSITIES (extra offset) - let saveButtonHorizontalOffset: CGFloat = 0 // ๐Ÿ’พ Save button horizontaal verschuiving - - // ๐Ÿ”„ BUTTON INTERACTIE & STYLING - let buttonHoverZoomScale: CGFloat = 1.1 // ๐Ÿ” Hover zoom effect (1.0 = geen zoom) - let buttonSpacing: CGFloat = 15 // โ†”๏ธ Afstand tussen buttons - let toolbarStartPadding: CGFloat = 8 // โ†โ†’ Afstand van linkerrand tot eerste button - - // ๐Ÿท FILENAME LABEL INSTELLINGEN - let filenameLabelFontSize: CGFloat = 10 // ๐Ÿ”ค Tekst grootte bestandsnaam - let filenameLabelVerticalOffset: CGFloat = 1 // ๐Ÿ“ Verticale positie bestandsnaam - let filenameLabelTextOpacity: CGFloat = 0.7 // ๐ŸŒซ Tekst doorzichtigheid - - // ๐ŸŽจ WINDOW STYLING INSTELLINGEN - let windowCornerRadius: CGFloat = 8 // ๐ŸŽจ Ronde hoeken window - let toolbarHeight: CGFloat = 20 // ๐Ÿ“ Hoogte van toolbar - let containerPadding: CGFloat = 5 // ๐Ÿ“ฆ Binnenste padding - let shadowPadding: CGFloat = 12 // ๐ŸŒซ Ruimte voor schaduw - - init(delegate: PreviewManagerDelegate) { - self.delegate = delegate - super.init() - - // Setup theme change observer for live preview updates - ThemeManager.shared.observeThemeChanges { [weak self] in - DispatchQueue.main.async { - self?.updateActivePreviewTheme() - } - } - } - - // MARK: - Theme Management - private func updateActivePreviewTheme() { - // Update only if there's an active preview window - guard let window = activePreviewWindow else { - print("๐ŸŽจ THEME: No active preview window to update") - return - } - - print("๐ŸŽจ THEME: Updating active preview window colors for current theme") - updateWindowThemeColors(window) - } - - private func updateWindowThemeColors(_ window: NSWindow) { - // Find and update all theme-dependent views recursively - updateViewThemeColors(window.contentView) - } - - private func updateViewThemeColors(_ view: NSView?) { - guard let view = view else { return } - - // Update container background colors - if view.layer?.backgroundColor != nil && view.layer?.backgroundColor != NSColor.clear.cgColor { - view.layer?.backgroundColor = ThemeManager.shared.containerBackground.cgColor - print("๐ŸŽจ THEME: Updated container background") - } - - // Update shadow colors - if view.layer?.shadowColor != nil { - view.layer?.shadowColor = ThemeManager.shared.shadowColor.cgColor - view.layer?.shadowOpacity = ThemeManager.shared.shadowOpacity - print("๐ŸŽจ THEME: Updated shadow colors") - } - - // Update button colors - if let button = view as? NSButton { - button.contentTintColor = ThemeManager.shared.buttonTintColor - // Update stored original color for hover restoration - objc_setAssociatedObject(button, "originalColor", ThemeManager.shared.buttonTintColor, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - print("๐ŸŽจ THEME: Updated button colors") - } - - // Update text field colors - if let textField = view as? NSTextField, textField.tag == 11001 { // Only filename labels - textField.textColor = ThemeManager.shared.filenameLabelTextColor - print("๐ŸŽจ THEME: Updated text field colors") - } - - // Recursively update subviews - for subview in view.subviews { - updateViewThemeColors(subview) - } - } - - // MARK: - Public Interface - func showPreview(image: NSImage) { - NSApp.activate(ignoringOtherApps: true) - print("๐Ÿ“ฆ Start showPreview") - print("๐Ÿ–ผ Afmeting: \(image.size)") - - // ๐Ÿ”„ SMART FIX: Only reset BGR mode for NEW screenshots, not BGR actions on existing thumbnails - if isBackgroundRemovalMode && !isBGRTransition { - print("๐Ÿ”„ FORCE RESET: Detected BGR mode active during NEW screenshot - forcing complete reset") - forceCompleteBGRReset() - } - - if let _ = self.activePreviewWindow { - print("๐Ÿ“ฆ Closing existing preview window before showing new one.") - // ๐Ÿ”ง CRITICAL FIX: Preserve tempURL when closing for new screenshot - self.closePreviewWithAnimation(immediate: true, preserveTempFile: true) - RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.05)) - } - - guard image.size.width > 0 && image.size.height > 0 else { - print("โŒ Lege afbeelding, preview wordt overgeslagen") - return - } - - delegate?.setLastImage(image) - - previewDismissTimer?.invalidate() - previewDismissTimer = nil - - guard let window = createNewThumbnailWindow(image: image) else { - print("โŒ Failed to create new thumbnail window in showPreview using createNewThumbnailWindow.") - if isPreviewUpdating { - DispatchQueue.main.async { self.isPreviewUpdating = false } - } - return - } - activePreviewWindow = window - - window.alphaValue = 1.0 - window.makeKeyAndOrderFront(nil as Any?) - - // Force window activation to ensure tracking areas work immediately - window.level = .floating - NSApp.activate(ignoringOtherApps: true) - - // Ensure window accepts mouse events - window.acceptsMouseMovedEvents = true - - print("DEBUG: showPreview FINISHED - Window: \(window), Visible: \(window.isVisible), Alpha: \(window.alphaValue), Frame: \(window.frame)") - - // Voeg automatisch sluiten timer toe volgens instellingen - let timerValue = SettingsManager.shared.thumbnailTimer - if timerValue > 0 { - previewDismissTimer = Timer.scheduledTimer(withTimeInterval: TimeInterval(timerValue), repeats: false) { [weak self] _ in - self?.closePreviewWithAnimation() - } - RunLoop.main.add(previewDismissTimer!, forMode: .common) - print("โฑ Scheduled auto-dismiss in \(timerValue) seconds") - } else { - print("โฑ Auto-dismiss disabled (thumbnailTimer = 0)") - } - } - - func updatePreviewSize() { - guard let lastImage = delegate?.getLastImage() else { return } - - if isClosing || isPreviewUpdating { return } - - isPreviewUpdating = true - - // Invalidate timer *before* closing, as closing might be asynchronous or delayed slightly - previewDismissTimer?.invalidate() - previewDismissTimer = nil - - closePreviewWithAnimation(immediate: true) - showPreview(image: lastImage) - - DispatchQueue.main.async { - self.isPreviewUpdating = false - } - } - - func closePreviewWithAnimation(immediate: Bool = false, preserveTempFile: Bool = false) { - // Invalidate timer at the very beginning of any close operation - previewDismissTimer?.invalidate() - previewDismissTimer = nil - - guard let window = activePreviewWindow, !isClosing else { - if activePreviewWindow == nil && isPreviewUpdating { - DispatchQueue.main.async { self.isPreviewUpdating = false } - } - return - } - - isClosing = true - - let cleanup = { [weak self] in - guard let self = self else { return } - window.orderOut(nil as Any?) - if self.activePreviewWindow === window { - print("๐Ÿงผ Cleaning up preview resources...") - self.activePreviewWindow = nil - self.currentImageView = nil - if !preserveTempFile { - self.delegate?.clearTempFile() - } else { - print("๐Ÿ’ฐ Preserving tempURL as requested.") - } - } - self.isClosing = false - } - - if immediate { - cleanup() - } else { - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.4 - window.animator().alphaValue = 0 - - // Voeg "hupje" animatie toe: beweeg venster iets omhoog - let currentFrame = window.frame - let bounceHeight: CGFloat = 20 // hoogte van het hupje - let finalFrame = NSRect(x: currentFrame.origin.x, - y: currentFrame.origin.y + bounceHeight, - width: currentFrame.width, - height: currentFrame.height) - window.animator().setFrame(finalFrame, display: true) - - }, completionHandler: cleanup) - } - } - - @objc func closePreviewWindow() { - previewDismissTimer?.invalidate() - previewDismissTimer = nil - closePreviewWithAnimation() - } - - func getActivePreviewWindow() -> NSWindow? { - return activePreviewWindow - } - - func ensurePreviewVisible() { - guard let preview = self.activePreviewWindow else { - if let img = delegate?.getLastImage() { - self.showPreview(image: img) - } - return - } - if !preview.isVisible { - preview.alphaValue = 0 - preview.orderFront(nil) - NSAnimationContext.runAnimationGroup { ctx in - ctx.duration = 2.0 - preview.animator().alphaValue = 1 - } - } - } - - // MARK: - Visual Effects - func flashPreviewBorder() { - guard let window = activePreviewWindow, let _ = window.contentView else { - print("โš ๏ธ FlashEffect: No active preview window or outerContainer (contentView).") - return - } - - guard let currentImgView = self.currentImageView, let imageContainer = currentImgView.superview else { - print("โš ๏ธ FlashEffect: Could not find imageContainer via currentImageView.superview.") - return - } - - let flashView = NSView(frame: imageContainer.bounds) - flashView.wantsLayer = true - flashView.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.50).cgColor - flashView.layer?.cornerRadius = imageContainer.layer?.cornerRadius ?? 10 - flashView.alphaValue = 0 - - imageContainer.addSubview(flashView) - - print("โœจ FlashEffect: Starting white flash animation on imageContainer.") - - NSAnimationContext.runAnimationGroup({ - context in - context.duration = 0.08 - context.timingFunction = CAMediaTimingFunction(name: .easeOut) - flashView.animator().alphaValue = 1.0 - }, completionHandler: { - NSAnimationContext.runAnimationGroup({ - context in - context.duration = 0.8 - context.timingFunction = CAMediaTimingFunction(name: .easeIn) - flashView.animator().alphaValue = 0.0 - }, completionHandler: { - flashView.removeFromSuperview() - print("โœจ FlashEffect: Animation complete, flashView removed.") - }) - }) - } - - // MARK: - Screen Selection for Thumbnail - func getTargetScreenForThumbnail() -> NSScreen? { - let setting = SettingsManager.shared.thumbnailDisplayScreen - - switch setting { - case .automatic: - let mouseLocation = NSEvent.mouseLocation - for screen in NSScreen.screens { - if screen.frame.contains(mouseLocation) { - print("๐Ÿ“บ Thumbnail scherm: Automatisch - muis op \(screen.localizedName)") - return screen - } - } - print("๐Ÿ“บ Thumbnail scherm: Automatisch fallback naar eerste scherm") - return NSScreen.screens.first ?? NSScreen.main - - case .screen1: - if NSScreen.screens.count >= 1 { - let screen = NSScreen.screens[0] - print("๐Ÿ“บ Thumbnail scherm: Scherm 1 (\(screen.localizedName))") - return screen - } - print("๐Ÿ“บ Thumbnail scherm: Scherm 1 niet beschikbaar, fallback naar eerste scherm") - return NSScreen.screens.first ?? NSScreen.main - - case .screen2: - if NSScreen.screens.count >= 2 { - let screen = NSScreen.screens[1] - print("๐Ÿ“บ Thumbnail scherm: Scherm 2 (\(screen.localizedName))") - return screen - } - print("๐Ÿ“บ Thumbnail scherm: Scherm 2 niet beschikbaar, fallback naar eerste scherm") - return NSScreen.screens.first ?? NSScreen.main - - case .screen3: - if NSScreen.screens.count >= 3 { - let screen = NSScreen.screens[2] - print("๐Ÿ“บ Thumbnail scherm: Scherm 3 (\(screen.localizedName))") - return screen - } - print("๐Ÿ“บ Thumbnail scherm: Scherm 3 niet beschikbaar, fallback naar eerste scherm") - return NSScreen.screens.first ?? NSScreen.main - - case .screen4: - if NSScreen.screens.count >= 4 { - let screen = NSScreen.screens[3] - print("๐Ÿ“บ Thumbnail scherm: Scherm 4 (\(screen.localizedName))") - return screen - } - print("๐Ÿ“บ Thumbnail scherm: Scherm 4 niet beschikbaar, fallback naar eerste scherm") - return NSScreen.screens.first ?? NSScreen.main - - case .screen5: - if NSScreen.screens.count >= 5 { - let screen = NSScreen.screens[4] - print("๐Ÿ“บ Thumbnail scherm: Scherm 5 (\(screen.localizedName))") - return screen - } - print("๐Ÿ“บ Thumbnail scherm: Scherm 5 niet beschikbaar, fallback naar eerste scherm") - return NSScreen.screens.first ?? NSScreen.main - } - } - - // MARK: - Cleanup - func cleanup() { - // Clean up BGR mode if active - if isBackgroundRemovalMode { - exitBackgroundRemovalMode() - } - - previewDismissTimer?.invalidate() - previewDismissTimer = nil - activePreviewWindow = nil - currentImageView = nil - } - - // MARK: - Window Creation - private func createNewThumbnailWindow(image: NSImage) -> NSWindow? { - if isPreviewUpdating { - print("DEBUG: createNewThumbnailWindow - isPreviewUpdating is true, returning nil early") - return nil - } - print("DEBUG: createNewThumbnailWindow called with image: \(image.size)") - - // Debug current theme - print("๐ŸŽจ THUMBNAIL: Creating new thumbnail window with current theme:") - ThemeManager.shared.printCurrentTheme() - - // 0. Basisinstellingen en afmetingen - let actualImageWidth = SettingsManager.shared.thumbnailFixedSize.dimensions.width - let actualImageHeight = SettingsManager.shared.thumbnailFixedSize.dimensions.height - - // 1. NIEUWE UNIFIED LAYOUT - รฉรฉn container voor alles (gebruik constants) - let fixedToolbarHeight: CGFloat = toolbarHeight // ๐ŸŽ› Gebruik constant - let spacingBelowToolbar: CGFloat = 8 - - // Corner Radii - gebruik constants - let mainContainerCornerRadius: CGFloat = windowCornerRadius // ๐ŸŽจ Gebruik constant - let imageCornerRadius: CGFloat = windowCornerRadius - 4 // ๐ŸŽจ Iets kleiner dan container - - // 2. Bereken afmetingen - UNIFIED - let imageWidth = actualImageWidth - let imageHeight = actualImageHeight - - // Main container bevat: padding + toolbar + spacing + image + padding - let mainContainerWidth = imageWidth + (2 * containerPadding) - let mainContainerHeight = containerPadding + fixedToolbarHeight + spacingBelowToolbar + imageHeight + containerPadding - - // 3. GLASS EFFECT CONTAINER - FIXED CORNER RADIUS - let shadowContainerFrame = NSRect(x: shadowPadding, - y: shadowPadding, - width: mainContainerWidth, - height: mainContainerHeight) - let mainContainerFrame = NSRect(x: 0, y: 0, width: mainContainerWidth, height: mainContainerHeight) - let mainContainer = NSVisualEffectView(frame: mainContainerFrame) - mainContainer.material = ThemeManager.shared.glassEffectMaterial - mainContainer.blendingMode = ThemeManager.shared.glassEffectBlending - mainContainer.state = .active - mainContainer.alphaValue = ThemeManager.shared.glassEffectAlpha - mainContainer.wantsLayer = true - - // ๐Ÿ”ฅ CRITICAL FIX: NSVisualEffectView corner radius - mainContainer.layer?.cornerRadius = mainContainerCornerRadius - mainContainer.layer?.masksToBounds = true // ๐Ÿ”ฅ MOET TRUE zijn voor corner radius! - - // ๐Ÿ”ฅ EXTRA FIX: Shadow op PARENT container, niet op visual effect view - let shadowContainer = NSView(frame: shadowContainerFrame) - shadowContainer.wantsLayer = true - shadowContainer.layer?.shadowColor = ThemeManager.shared.shadowColor.cgColor - shadowContainer.layer?.shadowOpacity = ThemeManager.shared.shadowOpacity - shadowContainer.layer?.shadowRadius = 6 - shadowContainer.layer?.shadowOffset = CGSize(width: 0, height: -2) - shadowContainer.layer?.cornerRadius = mainContainerCornerRadius - shadowContainer.layer?.masksToBounds = false // Shadow container mag wel shadow buiten bounds - - // 4. Toolbar BINNEN de main container (ONDERAAN) - let toolbarFrame = NSRect(x: containerPadding, - y: containerPadding, - width: mainContainerWidth - (2 * containerPadding), - height: fixedToolbarHeight) - let toolbarView = NSView(frame: toolbarFrame) - toolbarView.wantsLayer = false // Geen aparte layer, gebruik parent styling - - // Toolbar buttons - let symbolConfig = NSImage.SymbolConfiguration(pointSize: 14, weight: .regular) - let closeSymbolConfig = NSImage.SymbolConfiguration(pointSize: 12.5, weight: .regular) // ๐ŸŽฏ Kleinere icoon voor close button - var currentXButton: CGFloat = toolbarStartPadding - - let closeButtonY = (fixedToolbarHeight - closeButtonSize) / 2 + closeButtonVerticalOffset - let closeButton = HoverButton(frame: NSRect(x: currentXButton, y: closeButtonY, width: closeButtonSize, height: closeButtonSize)) - closeButton.image = NSImage(systemSymbolName: "xmark.circle", accessibilityDescription: "Close")?.withSymbolConfiguration(closeSymbolConfig) - closeButton.isBordered = false; closeButton.bezelStyle = .shadowlessSquare; closeButton.contentTintColor = ThemeManager.shared.buttonTintColor - closeButton.action = #selector(closePreviewWindow); closeButton.target = self - closeButton.setupHover(zoomScale: buttonHoverZoomScale) - toolbarView.addSubview(closeButton) - currentXButton += closeButtonSize + buttonSpacing - - let saveButtonY = (fixedToolbarHeight - saveButtonSize) / 2 + saveButtonVerticalOffset - let saveButtonX = currentXButton + saveButtonHorizontalOffset - let saveButton = HoverButton(frame: NSRect(x: saveButtonX, y: saveButtonY, width: saveButtonSize, height: saveButtonSize)) - saveButton.image = NSImage(systemSymbolName: "folder.fill.badge.plus", accessibilityDescription: "Save")?.withSymbolConfiguration(symbolConfig) - saveButton.isBordered = false; saveButton.bezelStyle = .shadowlessSquare; saveButton.contentTintColor = ThemeManager.shared.buttonTintColor - saveButton.action = #selector(PreviewManager.saveFromPreview); saveButton.target = self - saveButton.setupHover(zoomScale: buttonHoverZoomScale) - toolbarView.addSubview(saveButton) - currentXButton += saveButtonSize + buttonSpacing - - var folderButton: HoverButton? - if SettingsManager.shared.showFolderButton { - let folderButtonY = (fixedToolbarHeight - folderButtonSize) / 2 + folderButtonVerticalOffset - folderButton = HoverButton(frame: NSRect(x: currentXButton, y: folderButtonY, width: folderButtonSize, height: folderButtonSize)) - folderButton!.image = NSImage(systemSymbolName: "folder.fill", accessibilityDescription: "Open Folder")?.withSymbolConfiguration(symbolConfig) - folderButton!.isBordered = false; folderButton!.bezelStyle = .shadowlessSquare; folderButton!.contentTintColor = ThemeManager.shared.buttonTintColor - folderButton!.action = #selector(PreviewManager.openScreenshotFolder); folderButton!.target = self - folderButton!.setupHover(zoomScale: buttonHoverZoomScale) - toolbarView.addSubview(folderButton!) - currentXButton += folderButtonSize + buttonSpacing - } - - let settingsButtonX = (mainContainerWidth - (2 * containerPadding)) - settingsButtonSize - 8 - let settingsButtonY = (fixedToolbarHeight - settingsButtonSize) / 2 + settingsButtonVerticalOffset - let settingsButton = HoverButton(frame: NSRect(x: settingsButtonX, y: settingsButtonY, width: settingsButtonSize, height: settingsButtonSize)) - settingsButton.image = NSImage(systemSymbolName: "gearshape.fill", accessibilityDescription: "Settings")?.withSymbolConfiguration(symbolConfig) - settingsButton.isBordered = false; settingsButton.bezelStyle = .shadowlessSquare; settingsButton.contentTintColor = ThemeManager.shared.buttonTintColor - settingsButton.action = #selector(PreviewManager.openSettings); settingsButton.target = self - settingsButton.setupHover(zoomScale: buttonHoverZoomScale) - toolbarView.addSubview(settingsButton) - - let labelX = currentXButton - let labelWidth = settingsButtonX - labelX - 8 - let labelDesiredHeight: CGFloat = 15.0 - let labelYOffset = (fixedToolbarHeight - labelDesiredHeight) / 2.0 + filenameLabelVerticalOffset - let filenameLabel = NSTextField(frame: NSRect(x: labelX, y: labelYOffset, width: labelWidth, height: labelDesiredHeight)) - filenameLabel.isEditable = false; filenameLabel.isSelectable = true; filenameLabel.isBordered = false - filenameLabel.backgroundColor = NSColor.clear; filenameLabel.textColor = ThemeManager.shared.filenameLabelTextColor - filenameLabel.font = NSFont.systemFont(ofSize: filenameLabelFontSize) - filenameLabel.alignment = .center - filenameLabel.lineBreakMode = .byTruncatingMiddle; filenameLabel.tag = 11001 - if let url = delegate?.getTempURL() { - filenameLabel.stringValue = url.lastPathComponent; filenameLabel.toolTip = url.lastPathComponent - } else { - let fallbackName = "Schermafbeelding \(DateFormatter().string(from: Date())).png" - filenameLabel.stringValue = fallbackName; filenameLabel.toolTip = fallbackName - } - toolbarView.addSubview(filenameLabel) - - // 5. Image Container BINNEN de main container (BOVENAAN) - let imageFrame = NSRect(x: containerPadding, - y: containerPadding + fixedToolbarHeight + spacingBelowToolbar, - width: imageWidth, - height: imageHeight) - let imageContainer = NSView(frame: imageFrame) - imageContainer.wantsLayer = true - imageContainer.layer?.cornerRadius = imageCornerRadius - imageContainer.layer?.masksToBounds = true - imageContainer.layer?.backgroundColor = NSColor.clear.cgColor - - // 6. DraggableImageView DIRECT in image container - let imageView = DraggableImageView(frame: imageContainer.bounds) - imageView.wantsLayer = true - imageView.clipsToBounds = true - imageView.image = image - imageView.imageScaling = .scaleProportionallyUpOrDown - imageView.imageAlignment = .alignCenter - imageView.layer?.cornerRadius = imageCornerRadius - imageView.layer?.masksToBounds = true - imageView.layer?.backgroundColor = NSColor.clear.cgColor - imageView.appDelegate = delegate as? ScreenshotApp - - // ๐Ÿ”ง FIX: Set imageURL for dragging functionality - // In BGR mode, this is set elsewhere, but for normal screenshots we need the current tempURL - if !isBackgroundRemovalMode { - imageView.imageURL = delegate?.getTempURL() - print("๐Ÿ”ง DraggableImageView: Set imageURL for normal screenshot: \(imageView.imageURL?.lastPathComponent ?? "nil")") - } - - currentImageView = imageView - imageContainer.addSubview(imageView) - - // 7. Window Assembly - SIMPLIFIED - let totalWindowWidth = mainContainerWidth + (2 * shadowPadding) - let totalWindowHeight = mainContainerHeight + (2 * shadowPadding) - let windowFrame = NSRect(x: 0, y: 0, width: totalWindowWidth, height: totalWindowHeight) - - let window = NSWindow(contentRect: windowFrame, styleMask: [.borderless], backing: .buffered, defer: false) - window.level = .floating - window.isOpaque = false - window.backgroundColor = .clear - window.hasShadow = false - - let rootView = NSView(frame: window.contentRect(forFrameRect: window.frame)) - rootView.wantsLayer = true - rootView.layer?.masksToBounds = false - rootView.layer?.backgroundColor = NSColor.clear.cgColor - window.contentView = rootView - - // FIXED HIERARCHY: root -> shadowContainer -> mainContainer -> (toolbar + imageContainer) - shadowContainer.addSubview(mainContainer) - rootView.addSubview(shadowContainer) - mainContainer.addSubview(toolbarView) - mainContainer.addSubview(imageContainer) - - // Debug prints - print("DEBUG: Total Window Frame: \(windowFrame)") - print("DEBUG: Main Container Frame: \(mainContainer.frame)") - print("DEBUG: Toolbar Frame: \(toolbarView.frame)") - print("DEBUG: Image Container Frame: \(imageContainer.frame)") - - // 5. Positioneer het window op het juiste scherm - guard let screen = getTargetScreenForThumbnail() else { - print("ERROR: Kon doelscherm niet bepalen") - return nil - } - let screenVisibleFrame = screen.visibleFrame - let screenEdgePadding: CGFloat = 20 - let windowOriginX = screenVisibleFrame.maxX - totalWindowWidth - screenEdgePadding - let windowOriginY = screenVisibleFrame.minY + screenEdgePadding - window.setFrameOrigin(NSPoint(x: windowOriginX, y: windowOriginY)) - - return window - } - - // MARK: - Loading Indicator - func showLoadingIndicator() { - guard !isShowingLoadingIndicator else { return } - - print("๐Ÿ”„ ShowLoadingIndicator: Starting loading indicator...") - isShowingLoadingIndicator = true - - // Bepaal de positie en grootte op basis van de thumbnail locatie - guard let targetScreen = getTargetScreenForThumbnail() else { - print("โŒ Could not determine target screen for loading indicator") - return - } - - // AANGEPAST: Nieuwe afmetingen - breder en minder hoog - let loadingSize = NSSize(width: 200, height: 70) - let screenVisibleFrame = targetScreen.visibleFrame - let screenEdgePadding: CGFloat = 20 - - // Plaats de loading indicator op dezelfde positie als waar de thumbnail zou komen - let loadingOriginX = screenVisibleFrame.maxX - loadingSize.width - screenEdgePadding - let loadingOriginY = screenVisibleFrame.minY + screenEdgePadding - - let loadingFrame = NSRect(x: loadingOriginX, y: loadingOriginY, width: loadingSize.width, height: loadingSize.height) - - // Creรซer het loading overlay window - let loadingWindow = NSWindow( - contentRect: loadingFrame, - styleMask: [.borderless], - backing: .buffered, - defer: false - ) - - loadingWindow.backgroundColor = NSColor.clear - loadingWindow.isOpaque = false - loadingWindow.hasShadow = true - loadingWindow.level = .floating - loadingWindow.ignoresMouseEvents = true - loadingWindow.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] - - // AANGEPAST: Start met alpha 0 voor fade-in animatie - loadingWindow.alphaValue = 0 - - // Creรซer de loading content view - let loadingContentView = NSView(frame: NSRect(origin: .zero, size: loadingSize)) - loadingContentView.wantsLayer = true - loadingContentView.layer?.cornerRadius = 12 - loadingContentView.layer?.backgroundColor = ThemeManager.shared.containerBackground.cgColor - loadingContentView.layer?.shadowColor = ThemeManager.shared.shadowColor.cgColor - loadingContentView.layer?.shadowOpacity = ThemeManager.shared.shadowOpacity - loadingContentView.layer?.shadowRadius = 6 - loadingContentView.layer?.shadowOffset = CGSize(width: 0, height: -2) - - // AANGEPAST: Horizontale progress bar in plaats van spinning indicator - let progressBarWidth: CGFloat = 140 - let progressBarHeight: CGFloat = 6 - let progressBar = NSProgressIndicator(frame: NSRect( - x: (loadingSize.width - progressBarWidth) / 2, - y: 25, - width: progressBarWidth, - height: progressBarHeight - )) - progressBar.style = .bar - progressBar.isIndeterminate = true - progressBar.controlSize = .regular - progressBar.startAnimation(nil) - - // AANGEPAST: Label positionering aangepast voor nieuwe layout - let loadingLabel = NSTextField(frame: NSRect(x: 10, y: 40, width: loadingSize.width - 20, height: 20)) - loadingLabel.isEditable = false - loadingLabel.isSelectable = false - loadingLabel.isBordered = false - loadingLabel.backgroundColor = NSColor.clear - loadingLabel.textColor = ThemeManager.shared.primaryTextColor - loadingLabel.font = NSFont.systemFont(ofSize: 13, weight: .medium) - loadingLabel.alignment = .center - loadingLabel.stringValue = "Stitching Screens..." - - loadingContentView.addSubview(progressBar) - loadingContentView.addSubview(loadingLabel) - loadingWindow.contentView = loadingContentView - - // Toon het window - loadingWindow.makeKeyAndOrderFront(nil) - loadingOverlayWindow = loadingWindow - - // NIEUW: Fade-in animatie - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.3 - context.timingFunction = CAMediaTimingFunction(name: .easeOut) - loadingWindow.animator().alphaValue = 1.0 - }, completionHandler: { - print("โœ… Loading indicator fade-in complete at \(loadingFrame)") - }) - } - - func hideLoadingIndicator() { - guard isShowingLoadingIndicator, let loadingWindow = loadingOverlayWindow else { return } - - print("๐Ÿ”„ HideLoadingIndicator: Hiding loading indicator...") - isShowingLoadingIndicator = false - - // AANGEPAST: Fade-out animatie - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.4 - context.timingFunction = CAMediaTimingFunction(name: .easeIn) - loadingWindow.animator().alphaValue = 0 - }, completionHandler: { [weak self] in - loadingWindow.orderOut(nil) - self?.loadingOverlayWindow = nil - print("โœ… Loading indicator fade-out complete and window closed") - }) - } - - // MARK: - Button Actions (delegate to main app) - @objc func saveFromPreview() { - delegate?.saveFromPreview(self) - } - - @objc func openScreenshotFolder() { - delegate?.openScreenshotFolder() - } - - @objc func openSettings() { - delegate?.openSettings(self) - } - - func closePreview() { - print("๐Ÿงผ Cleaning up preview resources...") - - // FIXED: Bewaar de huidige tempURL voordat we gaan clearen - let currentTempURL = delegate?.getTempURL() - - // Close window first - if let window = activePreviewWindow { - window.orderOut(nil) - window.close() - } - activePreviewWindow = nil - - // FIXED: Alleen de tempURL clearen als er geen nieuwe screenshot bezig is - // Dit voorkomt dat we de net gemaakte screenshot URL wegwissen - if let tempURL = currentTempURL { - let filename = tempURL.lastPathComponent - // Controleer of dit de oude screenshot is of een nieuwe - // Als de timestamp meer dan 5 seconden oud is, clean het op - let now = Date() - let fileCreationTime = (try? FileManager.default.attributesOfItem(atPath: tempURL.path)[.creationDate] as? Date) ?? now - let timeDifference = now.timeIntervalSince(fileCreationTime) - - if timeDifference > 5.0 { - // Dit is een oude screenshot, veilig om op te ruimen - delegate?.setTempFileURL(nil) - print("๐Ÿ—‘๏ธ Cleaned up old screenshot file: \(filename)") - } else { - // Dit is een nieuwe screenshot, NIET opruimen - print("โš ๏ธ Preserving recent screenshot file: \(filename) (created \(String(format: "%.1f", timeDifference))s ago)") - } - } - } -} - -// MARK: - Custom Tracking Area -private class HoverTrackingArea: NSTrackingArea { - // This will be used to identify our custom tracking areas -} - -// MARK: - PreviewManager Delegate Protocol -protocol PreviewManagerDelegate: AnyObject { - func getLastImage() -> NSImage? - func setLastImage(_ image: NSImage) - func clearTempFile() - func getTempURL() -> URL? - func findFilenameLabel(in window: NSWindow?) -> NSTextField? - func openScreenshotFolder() - func openSettings(_ sender: Any?) - func saveFromPreview(_ sender: Any) - func setTempFileURL(_ url: URL?) -} - -// MARK: - Custom Button with Built-in Hover -class HoverButton: NSButton { - private var hoverHandler: ButtonHoverHandler? - private var trackingArea: NSTrackingArea? - - override func awakeFromNib() { - super.awakeFromNib() - setupHover() - } - - func setupHover(zoomScale: CGFloat = 1.3) { - wantsLayer = true - - // Store current theme-aware color as original - let originalColor = ThemeManager.shared.buttonTintColor - - // Create hover handler with custom zoom scale - hoverHandler = ButtonHoverHandler(button: self, zoomScale: zoomScale) - - // Store theme-aware original color - objc_setAssociatedObject(self, "originalColor", originalColor, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - - // Setup tracking area - updateTrackingAreas() - - print("๐ŸŽฏ SETUP: HoverButton setup complete with \(zoomScale)x zoom and theme-aware colors") - } - - override func updateTrackingAreas() { - super.updateTrackingAreas() - - // Remove existing tracking area - if let existing = trackingArea { - removeTrackingArea(existing) - } - - // Create new tracking area - trackingArea = NSTrackingArea( - rect: bounds, - options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect], - owner: self, - userInfo: nil - ) - if let trackingArea = trackingArea { - addTrackingArea(trackingArea) - print("๐ŸŽฏ SETUP: HoverButton tracking area added") - } - } - - override func mouseEntered(with event: NSEvent) { - super.mouseEntered(with: event) - print("๐ŸŽฏ HOVER: HoverButton mouseEntered") - hoverHandler?.mouseEntered(with: event) - } - - override func mouseExited(with event: NSEvent) { - super.mouseExited(with: event) - print("๐ŸŽฏ HOVER: HoverButton mouseExited") - hoverHandler?.mouseExited(with: event) - } -} - -// MARK: - BGR Overlay Button with Hover Tooltips -class BGROverlayButton: NSButton { - enum TooltipPosition { - case left, right - } - - var tooltipText: String = "" - var tooltipPosition: TooltipPosition = .left - weak var parentContainer: NSView? - - private var tooltipView: NSView? - private var trackingArea: NSTrackingArea? - - override func updateTrackingAreas() { - super.updateTrackingAreas() - - // Remove existing tracking area - if let existing = trackingArea { - removeTrackingArea(existing) - } - - // Create new tracking area - trackingArea = NSTrackingArea( - rect: bounds, - options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect], - owner: self, - userInfo: nil - ) - if let trackingArea = trackingArea { - addTrackingArea(trackingArea) - print("๐ŸŽฏ BGR Button: Tracking area added - bounds: \(bounds), tooltipText: '\(tooltipText)'") - } - } - - override func mouseEntered(with event: NSEvent) { - super.mouseEntered(with: event) - print("๐ŸŽฏ BGR Button: Mouse entered - tooltipText: '\(tooltipText)'") - showTooltip() - } - - override func mouseExited(with event: NSEvent) { - super.mouseExited(with: event) - print("๐ŸŽฏ BGR Button: Mouse exited") - hideTooltip() - } - - private func showTooltip() { - guard !tooltipText.isEmpty, - let container = parentContainer else { - print("๐Ÿšซ Tooltip: Missing text or container - text: '\(tooltipText)', container: \(parentContainer != nil)") - return - } - - // Force cleanup any existing tooltip first - if let existingTooltip = tooltipView { - print("๐Ÿงน Tooltip: Cleaning up existing tooltip before showing new one") - existingTooltip.removeFromSuperview() - tooltipView = nil - } - - print("โœจ Tooltip: Showing '\(tooltipText)' at position \(tooltipPosition)") - - // Create simple text tooltip (no button/container styling) - let tooltipLabel = NSTextField() - tooltipLabel.stringValue = tooltipText - tooltipLabel.textColor = .white - tooltipLabel.backgroundColor = .clear - tooltipLabel.font = NSFont.systemFont(ofSize: 11, weight: .medium) - tooltipLabel.alignment = .center - tooltipLabel.isBordered = false - tooltipLabel.isEditable = false - tooltipLabel.isSelectable = false - tooltipLabel.sizeToFit() - - // Add subtle text shadow for readability - tooltipLabel.wantsLayer = true - tooltipLabel.layer?.shadowColor = NSColor.black.cgColor - tooltipLabel.layer?.shadowOpacity = 0.8 - tooltipLabel.layer?.shadowRadius = 2 - tooltipLabel.layer?.shadowOffset = CGSize(width: 0, height: -1) - - // Position text tooltip relative to button in container coordinates - let buttonFrameInContainer = self.frame - let buttonCenterY = buttonFrameInContainer.midY - let tooltipY = buttonCenterY - (tooltipLabel.frame.height / 2) - - let tooltipX: CGFloat - let spacing: CGFloat = 8 - - switch tooltipPosition { - case .left: - tooltipX = buttonFrameInContainer.minX - tooltipLabel.frame.width - spacing - case .right: - tooltipX = buttonFrameInContainer.maxX + spacing - } - - tooltipLabel.frame = NSRect(x: tooltipX, y: tooltipY, width: tooltipLabel.frame.width, height: tooltipLabel.frame.height) - - print("๐Ÿ“ Tooltip: Button frame: \(buttonFrameInContainer), Tooltip frame: \(tooltipLabel.frame)") - - // Add directly to container (no wrapper view) - container.addSubview(tooltipLabel) - - // Fade in animation - tooltipLabel.alphaValue = 0 - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.2 - tooltipLabel.animator().alphaValue = 1.0 - } - - self.tooltipView = tooltipLabel - print("โœ… Tooltip: Added to container with \(container.subviews.count) total subviews") - } - - private func hideTooltip() { - guard let tooltip = tooltipView else { - print("๐Ÿšซ Tooltip: No tooltip to hide") - return - } - - print("๐ŸŽฏ Tooltip: Hiding tooltip - current alpha: \(tooltip.alphaValue)") - - // Force immediate removal without animation to prevent hanging - tooltip.removeFromSuperview() - self.tooltipView = nil - print("โœ… Tooltip: Force removed from view") - } - - // ๐Ÿ”ง NEW: Public method to force hide tooltips (for cleanup) - func forceHideTooltip() { - hideTooltip() - } -} - -// MARK: - Background Removal Mode Extension -extension PreviewManager { - - // ๐ŸŽจ NEW: Show BGR preview with identical thumbnail UI - func showBackgroundRemovalPreview(originalImage: NSImage, originalURL: URL) { - print("๐ŸŽจ PreviewManager: Starting BGR preview with thumbnail UI") - - // ๐Ÿ”„ NEW: Set transition flag to prevent force reset during BGR action - isBGRTransition = true - - // Store the original image - self.originalImage = originalImage - self.processedImage = nil - self.isShowingProcessedImage = false - - // Store original image URL from parameter (not delegate which is cleared) - self.originalImageURL = originalURL - self.processedImageURL = nil - - print("๐ŸŽจ BGR: Stored original URL: \(originalURL.path)") - - // Enable BGR mode - isBackgroundRemovalMode = true - - // ๐Ÿ”ง CRITICAL FIX: Check if there's already an active preview window and user has closeAfterDrag OFF - let hasActivePreview = activePreviewWindow != nil - let shouldReusePreview = hasActivePreview && !SettingsManager.shared.closeAfterDrag - - if shouldReusePreview { - print("๐Ÿ”„ BGR: Reusing existing preview window (closeAfterDrag is OFF)") - - // Update existing window for BGR mode - if let window = activePreviewWindow { - window.level = .popUpMenu // Higher level than .floating to stay on top - print("๐ŸŽจ BGR: Set window level to .popUpMenu for always-on-top behavior") - } - - // Update existing image view - if let imageView = currentImageView { - imageView.image = originalImage - imageView.imageURL = originalImageURL - print("๐ŸŽจ BGR: Updated existing imageView with BGR image and URL") - } - - // Update filename label to show original filename in BGR mode - updateBGRFilenameLabel() - - } else { - print("๐Ÿ”„ BGR: Creating new preview window (closeAfterDrag is ON or no existing preview)") - - // Use existing thumbnail system to create new window - showPreview(image: originalImage) - - // ๐ŸŽจ NEW: Set higher window level for BGR mode to stay on top - if let window = activePreviewWindow { - window.level = .popUpMenu // Higher level than .floating to stay on top - print("๐ŸŽจ BGR: Set window level to .popUpMenu for always-on-top behavior") - } - - // ๐ŸŽจ NEW: Initialize DraggableImageView with original URL for BGR mode - if isBackgroundRemovalMode { - currentImageView?.imageURL = originalImageURL - print("๐ŸŽจ BGR: Set initial imageURL to original: \(originalImageURL?.path ?? "nil")") - } - - // ๐ŸŽจ NEW: Update filename label to show original filename in BGR mode - updateBGRFilenameLabel() - } - - // Add BGR overlay buttons (for both cases) - DispatchQueue.main.async { - self.addBGROverlayButtons() - - // Start automatic BGR processing - self.startBackgroundRemovalProcessing() - - // ๐Ÿ”„ NEW: Clear transition flag after BGR setup is complete - self.isBGRTransition = false - print("๐Ÿ”„ BGR transition flag cleared - normal state management resumed") - } - } - - // ๐ŸŽจ NEW: Add small overlay buttons IN the image area - private func addBGROverlayButtons() { - guard let _ = activePreviewWindow, - let imageView = currentImageView, - let imageContainer = imageView.superview else { - print("โŒ Cannot add BGR buttons: missing components") - return - } - - let buttonSize: CGFloat = 28 - let buttonSpacing: CGFloat = 8 - let bottomMargin: CGFloat = 12 - - // Position buttons at bottom center of image - let totalButtonsWidth = (buttonSize * 2) + buttonSpacing - let startX = (imageContainer.bounds.width - totalButtonsWidth) / 2 - let buttonY = bottomMargin - - // Reset button (โ†บ) - let resetButton = createBGROverlayButton( - frame: NSRect(x: startX, y: buttonY, width: buttonSize, height: buttonSize), - symbolName: "arrow.clockwise" - ) - resetButton.action = #selector(bgrResetButtonClicked) - resetButton.target = self - resetButton.isHidden = true // Initially hidden until processing is complete - resetButton.tooltipText = "Reset" - resetButton.tooltipPosition = .left - resetButton.parentContainer = imageContainer - - // Toggle button (๐Ÿ‘) - let toggleButton = createBGROverlayButton( - frame: NSRect(x: startX + buttonSize + buttonSpacing, y: buttonY, width: buttonSize, height: buttonSize), - symbolName: "eye" - ) - toggleButton.action = #selector(bgrToggleButtonClicked) - toggleButton.target = self - toggleButton.isHidden = true // Initially hidden until processing is complete - toggleButton.tooltipText = "Compare" - toggleButton.tooltipPosition = .right - toggleButton.parentContainer = imageContainer - - // Add to image container - imageContainer.addSubview(resetButton) - imageContainer.addSubview(toggleButton) - - // Store references - bgrResetButton = resetButton - bgrToggleButton = toggleButton - - print("๐ŸŽจ BGR overlay buttons added to image container") - } - - // ๐ŸŽจ NEW: Add BGR progress bar in center of image - private func addBGRProgressBar() { - guard let _ = activePreviewWindow, - let imageView = currentImageView, - let imageContainer = imageView.superview else { - print("โŒ Cannot add BGR progress bar: missing components") - return - } - - // Progress container dimensions - let containerWidth: CGFloat = 200 - let containerHeight: CGFloat = 60 - let containerX = (imageContainer.bounds.width - containerWidth) / 2 - let containerY = (imageContainer.bounds.height - containerHeight) / 2 - - // Create progress container with glass effect - let progressContainer = NSView(frame: NSRect(x: containerX, y: containerY, width: containerWidth, height: containerHeight)) - progressContainer.wantsLayer = true - progressContainer.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.7).cgColor - progressContainer.layer?.cornerRadius = 12 - progressContainer.layer?.masksToBounds = true - progressContainer.layer?.borderColor = NSColor.white.withAlphaComponent(0.2).cgColor - progressContainer.layer?.borderWidth = 1 - - // Create progress bar - let progressBarWidth: CGFloat = 160 - let progressBarHeight: CGFloat = 6 - let progressBarX = (containerWidth - progressBarWidth) / 2 - let progressBarY = containerHeight / 2 + 8 - - let progressBar = NSProgressIndicator(frame: NSRect(x: progressBarX, y: progressBarY, width: progressBarWidth, height: progressBarHeight)) - progressBar.style = .bar - progressBar.isIndeterminate = false - progressBar.minValue = 0 - progressBar.maxValue = 100 - progressBar.doubleValue = 0 - progressBar.wantsLayer = true - progressBar.layer?.cornerRadius = 3 - - // Create progress label - let progressLabel = NSTextField(frame: NSRect(x: 0, y: containerHeight / 2 - 20, width: containerWidth, height: 20)) - progressLabel.stringValue = "Removing background... 0%" - progressLabel.textColor = .white - progressLabel.backgroundColor = .clear - progressLabel.font = NSFont.systemFont(ofSize: 12, weight: .medium) - progressLabel.alignment = .center - progressLabel.isBordered = false - progressLabel.isEditable = false - progressLabel.isSelectable = false - - // Add subviews - progressContainer.addSubview(progressBar) - progressContainer.addSubview(progressLabel) - imageContainer.addSubview(progressContainer) - - // Store references - bgrProgressContainer = progressContainer - bgrProgressBar = progressBar - bgrProgressLabel = progressLabel - - // Fade in animation - progressContainer.alphaValue = 0 - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.3 - progressContainer.animator().alphaValue = 1.0 - } - - print("๐ŸŽจ BGR progress bar added to image container") - } - - // ๐ŸŽจ NEW: Update BGR progress bar - private func updateBGRProgress(_ progress: Double, status: String) { - DispatchQueue.main.async { - self.bgrProgressBar?.doubleValue = progress - self.bgrProgressLabel?.stringValue = "\(status) \(Int(progress))%" - } - } - - // ๐ŸŽจ NEW: Hide BGR progress bar - private func hideBGRProgressBar() { - guard let progressContainer = bgrProgressContainer else { return } - - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.4 - progressContainer.animator().alphaValue = 0 - }) { - progressContainer.removeFromSuperview() - self.bgrProgressContainer = nil - self.bgrProgressBar = nil - self.bgrProgressLabel = nil - } - } - - // ๐ŸŽจ NEW: Create clean BGR overlay button (icon only, no button styling) - private func createBGROverlayButton(frame: NSRect, symbolName: String) -> BGROverlayButton { - let button = BGROverlayButton(frame: frame) - - // Completely transparent button - no background or borders - button.wantsLayer = true - button.layer?.backgroundColor = NSColor.clear.cgColor - - // No button styling - button.isBordered = false - button.bezelStyle = .shadowlessSquare - - // SF Symbol icon with subtle shadow for visibility (20% smaller: 16pt -> 13pt) - let symbolConfig = NSImage.SymbolConfiguration(pointSize: 13, weight: .semibold) - button.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)?.withSymbolConfiguration(symbolConfig) - button.contentTintColor = .white - - // Add subtle shadow to icon for better visibility - button.layer?.shadowColor = NSColor.black.cgColor - button.layer?.shadowOpacity = 0.7 - button.layer?.shadowRadius = 2 - button.layer?.shadowOffset = CGSize(width: 0, height: -1) - - // Ensure tracking areas are set up - button.updateTrackingAreas() - - return button - } - - // ๐ŸŽจ NEW: Start BGR processing automatically - private func startBackgroundRemovalProcessing() { - guard let originalImg = originalImage else { - print("โŒ No original image for BGR processing") - return - } - - print("๐ŸŽจ Starting automatic BGR processing with user's preferred method...") - - // Show progress bar - addBGRProgressBar() - - // Store processing result to be accessed by progress checker - var processingResult: NSImage? = nil - var isProcessingComplete = false - - // Start actual background removal processing - BackgroundRemover.shared.processWithPreferredMethod(from: originalImg) { [weak self] result in - processingResult = result - isProcessingComplete = true - print("๐ŸŽจ BGR: Background removal processing completed with result: \(result != nil ? "success" : "failed")") - } - - // Start progress simulation that waits for actual completion - simulateBGRProgress( - checkProcessingComplete: { isProcessingComplete }, - getProcessingResult: { processingResult } - ) { [weak self] finalResult in - // This runs when both progress and processing are complete - DispatchQueue.main.async { - // Hide progress bar - self?.hideBGRProgressBar() - - if let processedImg = finalResult { - print("โœ… BGR processing completed successfully") - self?.processedImage = processedImg - - // ๐ŸŽจ NEW: Save processed image to file for dragging - self?.saveProcessedImageToFile(processedImg) - - // Update image view to show processed image - self?.currentImageView?.image = processedImg - self?.isShowingProcessedImage = true - - // ๐ŸŽจ NEW: Update DraggableImageView to use processed image URL - self?.updateDraggableImageURL() - - // ๐ŸŽจ NEW: Update filename label to show processed filename - self?.updateBGRFilenameLabel() - - // Show overlay buttons - self?.bgrResetButton?.isHidden = false - self?.bgrToggleButton?.isHidden = false - - // Add subtle animation to reveal buttons - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.5 - self?.bgrResetButton?.animator().alphaValue = 1.0 - self?.bgrToggleButton?.animator().alphaValue = 1.0 - } - - } else { - print("โŒ BGR processing failed") - // Keep original image showing, don't show overlay buttons - } - } - } - } - - // ๐ŸŽจ NEW: Smart BGR progress that adapts to actual processing speed - private func simulateBGRProgress( - checkProcessingComplete: @escaping () -> Bool, - getProcessingResult: @escaping () -> NSImage?, - completion: @escaping (NSImage?) -> Void - ) { - let processingStartTime = Date() - var currentProgress: Double = 0 - - // Start monitoring actual completion immediately - func checkForEarlyCompletion() { - let elapsedTime = Date().timeIntervalSince(processingStartTime) - - // Check if processing is already complete - if checkProcessingComplete() { - let result = getProcessingResult() - print("๐Ÿš€ FAST COMPLETION: BGR finished in \(String(format: "%.1f", elapsedTime)) seconds!") - - // Rapidly complete the progress bar - finishProgressBar(with: result, completion: completion) - return - } - - // Update progress based on elapsed time with smart acceleration - let targetProgress: Double - if elapsedTime < 1.0 { - // Very fast progress for Vision Framework - targetProgress = min(70.0, elapsedTime * 70.0) - updateBGRProgress(targetProgress, status: "Processing...") - } else if elapsedTime < 2.0 { - // Continue fast for Vision Framework - targetProgress = 70.0 + ((elapsedTime - 1.0) * 15.0) - updateBGRProgress(targetProgress, status: "Applying mask...") - } else if elapsedTime < 3.0 { - // Near completion for Vision Framework - targetProgress = 85.0 + ((elapsedTime - 2.0) * 5.0) - updateBGRProgress(targetProgress, status: "Finalizing...") - } else { - // Fallback for slower methods (RMBG with E5RT issues) - targetProgress = min(90.0, 90.0 + ((elapsedTime - 3.0) * 0.5)) - updateBGRProgress(targetProgress, status: "Processing...") - } - - currentProgress = targetProgress - - // Check again in 0.2 seconds for very responsive updates - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - checkForEarlyCompletion() - } - } - - // Start progress immediately - updateBGRProgress(5.0, status: "Starting...") - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - checkForEarlyCompletion() - } - } - - // ๐ŸŽจ NEW: Rapidly finish progress bar when processing completes - private func finishProgressBar(with result: NSImage?, completion: @escaping (NSImage?) -> Void) { - // Smoothly animate to 100% - updateBGRProgress(95.0, status: "Complete!") - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - self.updateBGRProgress(100.0, status: "Complete!") - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { - completion(result) - } - } - } - - // ๐ŸŽจ NEW: Wait for actual BGR processing to complete - private func waitForActualCompletion( - checkComplete: @escaping () -> Bool, - getResult: @escaping () -> NSImage?, - completion: @escaping (NSImage?) -> Void - ) { - // Check every 0.5 seconds if processing is actually done - func checkCompletion() { - if checkComplete() { - print("๐ŸŽจ Actual BGR processing detected as complete!") - let result = getResult() - completion(result) - } else { - // Keep waiting and checking - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - checkCompletion() - } - } - } - - checkCompletion() - } - - // ๐ŸŽจ NEW: Reset to main thumbnail (exit BGR mode completely) - @objc private func bgrResetButtonClicked() { - guard let originalImg = originalImage else { return } - - print("๐Ÿ”„ BGR Reset: Exiting BGR mode and returning to main thumbnail") - - // Smooth animation to original image first - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.3 - self.currentImageView?.animator().alphaValue = 0.7 - }) { - self.currentImageView?.image = originalImg - - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.3 - self.currentImageView?.animator().alphaValue = 1.0 - }) { - // After animation completes, exit BGR mode completely - self.exitBackgroundRemovalMode() - - // Reset to normal thumbnail state - self.showNormalThumbnail(with: originalImg) - - // ๐Ÿ”„ NEW: Handle post-action completion just like main thumbnail - self.handleBGRActionCompletion(actionType: "reset") - } - } - - print("โœ… BGR Reset: Returned to main thumbnail - BGR buttons removed") - } - - // ๐ŸŽจ NEW: Show normal thumbnail (no BGR mode) - private func showNormalThumbnail(with image: NSImage) { - print("๐Ÿ”„ Restoring normal thumbnail state...") - - // Reset all BGR-specific properties (already done in exitBackgroundRemovalMode) - isBackgroundRemovalMode = false - isShowingProcessedImage = false - - // Reset window level to normal - if let window = activePreviewWindow { - window.level = .floating - print("๐ŸŽจ Reset window level to normal (.floating)") - } - - // ๐Ÿ”„ CRITICAL FIX: Enhanced URL validation and recovery - if let originalURL = originalImageURL { - print("๐Ÿ” Checking original URL validity: \(originalURL.path)") - - // Verify file accessibility - if FileManager.default.fileExists(atPath: originalURL.path) { - // File exists - set it properly - currentImageView?.imageURL = originalURL - print("โœ… DraggableImageView reset to verified original URL: \(originalURL.lastPathComponent)") - - // Double verify by trying to load the image - if NSImage(contentsOf: originalURL) != nil { - print("โœ… Original image file loads successfully") - } else { - print("โš ๏ธ WARNING: Original URL exists but image won't load") - } - - } else { - print("โŒ CRITICAL: Original file missing at reset time: \(originalURL.path)") - - // RECOVERY: Try multiple fallback strategies - var recoveredURL: URL? = nil - - // Strategy 1: Check if it's in temp directory - let tempDir = FileManager.default.temporaryDirectory - let fileName = originalURL.lastPathComponent - let tempURL = tempDir.appendingPathComponent(fileName) - if FileManager.default.fileExists(atPath: tempURL.path) { - recoveredURL = tempURL - print("๐Ÿ”„ RECOVERY: Found file in temp directory: \(tempURL.path)") - } - - // Strategy 2: Use DraggableImageView's current URL - if recoveredURL == nil, let fallbackURL = currentImageView?.imageURL { - if FileManager.default.fileExists(atPath: fallbackURL.path) { - recoveredURL = fallbackURL - print("๐Ÿ”„ RECOVERY: Using DraggableImageView fallback URL: \(fallbackURL.path)") - } - } - - // Strategy 3: Try to recreate the file from the current image - if recoveredURL == nil { - print("๐Ÿ”„ RECOVERY: Attempting to save current image to new temp file...") - let newTempURL = FileManager.default.temporaryDirectory.appendingPathComponent("recovered_\(UUID().uuidString).png") - - if let imageData = image.pngData() { - do { - try imageData.write(to: newTempURL) - recoveredURL = newTempURL - print("โœ… RECOVERY: Created new temp file: \(newTempURL.path)") - } catch { - print("โŒ RECOVERY: Failed to save recovery file: \(error)") - } - } - } - - // Apply the recovered URL - if let finalURL = recoveredURL { - originalImageURL = finalURL - currentImageView?.imageURL = finalURL - print("โœ… URL RECOVERED: Now using \(finalURL.lastPathComponent)") - } else { - print("โŒ RECOVERY FAILED: No valid URL available") - } - } - } else { - print("โŒ ERROR: No originalImageURL available for normal thumbnail!") - - // Last resort: Use DraggableImageView's current URL - if let fallbackURL = currentImageView?.imageURL { - originalImageURL = fallbackURL - print("๐Ÿ”„ EMERGENCY: Set originalImageURL from DraggableImageView: \(fallbackURL.lastPathComponent)") - } - } - - // Reset filename label to original - if let finalURL = originalImageURL, - let filenameLabel = delegate?.findFilenameLabel(in: activePreviewWindow) { - filenameLabel.stringValue = finalURL.lastPathComponent - filenameLabel.toolTip = finalURL.lastPathComponent - print("๐ŸŽจ Reset filename label to: \(finalURL.lastPathComponent)") - } - - print("โœ… Normal thumbnail state restored - ready for new actions") - } - - // ๐ŸŽจ NEW: Toggle between original and processed - @objc private func bgrToggleButtonClicked() { - guard let originalImg = originalImage, - let processedImg = processedImage else { return } - - // Determine current state and toggle - let nextImage = isShowingProcessedImage ? originalImg : processedImg - isShowingProcessedImage = !isShowingProcessedImage - let nextSymbol = isShowingProcessedImage ? "eye.slash" : "eye" - - print("๐Ÿ”„ BGR Toggle: Switching to \(isShowingProcessedImage ? "processed" : "original") image") - - // Update image with animation - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.3 - self.currentImageView?.animator().alphaValue = 0.7 - }) { - self.currentImageView?.image = nextImage - - // ๐ŸŽจ NEW: Update DraggableImageView URL for proper dragging - self.updateDraggableImageURL() - - // ๐ŸŽจ NEW: Update filename label to reflect current image - self.updateBGRFilenameLabel() - - NSAnimationContext.runAnimationGroup { context in - context.duration = 0.3 - self.currentImageView?.animator().alphaValue = 1.0 - } - } - - // Update toggle button icon - let symbolConfig = NSImage.SymbolConfiguration(pointSize: 14, weight: .semibold) - bgrToggleButton?.image = NSImage(systemSymbolName: nextSymbol, accessibilityDescription: nil)?.withSymbolConfiguration(symbolConfig) - } - - // ๐ŸŽจ NEW: Exit BGR mode and return to normal thumbnail - func exitBackgroundRemovalMode() { - print("๐ŸŽจ Exiting BGR mode") - - // ๐Ÿ”„ PRESERVE original image and URL for normal thumbnail to continue working - let preservedOriginalImage = originalImage - let preservedOriginalURL = originalImageURL - - // Use the force reset to clean everything up - forceCompleteBGRReset() - - // ๐Ÿ”„ CRITICAL: Restore the preserved values for normal thumbnail functionality - originalImage = preservedOriginalImage - originalImageURL = preservedOriginalURL - - print("โœ… Preserved originalImage and originalImageURL for normal thumbnail: \(originalImageURL?.lastPathComponent ?? "nil")") - - // Verify the file still exists and report detailed status - if let originalURL = originalImageURL { - if FileManager.default.fileExists(atPath: originalURL.path) { - print("โœ… VERIFIED: Original file exists and is accessible: \(originalURL.path)") - } else { - print("โŒ CRITICAL: Original file missing after BGR reset: \(originalURL.path)") - - // Try to get alternative URL from DraggableImageView - if let fallbackURL = currentImageView?.imageURL { - print("๐Ÿ”„ Attempting fallback URL: \(fallbackURL.path)") - if FileManager.default.fileExists(atPath: fallbackURL.path) { - originalImageURL = fallbackURL - print("โœ… Using fallback URL: \(fallbackURL.path)") - } - } - } - } - } - - // ๐ŸŽจ NEW: Save processed image to file for dragging - private func saveProcessedImageToFile(_ image: NSImage) { - guard let originalURL = originalImageURL else { - print("โŒ No original image URL found to save processed image") - return - } - - // Create processed image filename - let originalFilename = originalURL.deletingPathExtension().lastPathComponent - let processedFilename = "\(originalFilename)_BGR.png" - let processedURL = originalURL.deletingLastPathComponent().appendingPathComponent(processedFilename) - - // Save processed image to file - do { - if let pngData = image.pngData() { - try pngData.write(to: processedURL) - self.processedImageURL = processedURL - print("โœ… Processed image saved to: \(processedURL.path)") - } else { - print("โŒ Failed to convert processed image to PNG data") - } - } catch { - print("โŒ Error saving processed image: \(error)") - } - } - - // ๐ŸŽจ NEW: Update DraggableImageView to use processed image URL - private func updateDraggableImageURL() { - guard isBackgroundRemovalMode else { return } - - // Use the correct URL based on which image is currently showing - let currentURL = isShowingProcessedImage ? processedImageURL : originalImageURL - - // Update DraggableImageView to use the correct URL - if let url = currentURL { - currentImageView?.imageURL = url - print("โœ… DraggableImageView updated to use \(isShowingProcessedImage ? "processed" : "original") image URL: \(url.lastPathComponent)") - } else { - print("โŒ No URL available for \(isShowingProcessedImage ? "processed" : "original") image") - } - } - - // ๐ŸŽจ NEW: Update filename label to show original filename in BGR mode - private func updateBGRFilenameLabel() { - guard let originalURL = originalImageURL else { - print("โŒ No original image URL found to update filename label") - return - } - - // Determine which filename to show based on current image - let displayURL: URL - if isShowingProcessedImage, let processedURL = processedImageURL { - displayURL = processedURL - } else { - displayURL = originalURL - } - - // Update filename label to show correct filename in BGR mode - if let filenameLabel = delegate?.findFilenameLabel(in: activePreviewWindow) { - filenameLabel.stringValue = displayURL.lastPathComponent - filenameLabel.toolTip = displayURL.lastPathComponent - print("๐ŸŽจ BGR: Updated filename label to: \(displayURL.lastPathComponent)") - } else { - print("โŒ No filename label found in active preview window") - } - } - - // ๐ŸŽจ NEW: Handle post-action completion just like main thumbnail - private func handleBGRActionCompletion(actionType: String) { - print("๐ŸŽจ BGR Action Completion: Handling '\(actionType)' action completion") - - // BGR reset action should behave like a "cancel" action - no specific closing behavior - // The user explicitly chose to go back to the main thumbnail, so keep it visible - print("โ„น๏ธ BGR reset action: Keeping thumbnail visible after reset") - } - - // ๐Ÿ”„ CRITICAL: Force complete BGR state reset for new screenshots - private func forceCompleteBGRReset() { - print("๐Ÿ”„ FORCE RESET: Starting complete BGR state reset...") - - // 1. Force hide any hanging tooltips immediately - bgrResetButton?.forceHideTooltip() - bgrToggleButton?.forceHideTooltip() - - // 2. Remove BGR overlay buttons immediately - bgrResetButton?.removeFromSuperview() - bgrToggleButton?.removeFromSuperview() - bgrResetButton = nil - bgrToggleButton = nil - - // 3. Remove BGR progress bar immediately - if let progressContainer = bgrProgressContainer { - progressContainer.removeFromSuperview() - bgrProgressContainer = nil - bgrProgressBar = nil - bgrProgressLabel = nil - } - - // 4. Clean up BGR file references - if let processedURL = processedImageURL { - try? FileManager.default.removeItem(at: processedURL) - print("๐Ÿ—‘๏ธ Force cleaned processed image file: \(processedURL.lastPathComponent)") - } - processedImageURL = nil - - // 5. Reset all BGR mode properties to clean state - isBackgroundRemovalMode = false - isBGRTransition = false // ๐Ÿ”„ NEW: Also reset transition flag - originalImage = nil - processedImage = nil - originalImageURL = nil - isShowingProcessedImage = false - - // 6. Reset window level if needed - if let window = activePreviewWindow { - window.level = .floating - print("๐Ÿ”„ Reset window level to normal (.floating)") - } - - print("โœ… FORCE RESET: Complete BGR state reset finished - ready for normal thumbnail") - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/RenameActionHandler.swift b/ShotScreen/Sources/RenameActionHandler.swift deleted file mode 100755 index a570d72..0000000 --- a/ShotScreen/Sources/RenameActionHandler.swift +++ /dev/null @@ -1,711 +0,0 @@ -import AppKit -import UniformTypeIdentifiers - -// NIEUW: Voeg ConflictType enum definitie toe -enum ConflictType { - case renameOnly - case saveToFolder - // TODO: case moveToApplications // Als dit later nodig is -} - -// NIEUW: Custom TextFieldCell voor verticale aanpassing van tekst -class VerticallyAdjustedTextFieldCell: NSTextFieldCell { - var verticalOffset: CGFloat = 0 - // didSet is hier mogelijk niet effectief genoeg, we forceren de update in drawingRect - // { - // didSet { - // self.controlView?.needsDisplay = true - // } - // } - - override func drawingRect(forBounds rect: NSRect) -> NSRect { - var drawingRect = super.drawingRect(forBounds: rect) - // Een positieve offset verplaatst de tekst naar beneden - drawingRect.origin.y = drawingRect.origin.y + verticalOffset - if let controlView = self.controlView { - controlView.setNeedsDisplay(controlView.bounds) // Correcte aanroep met bounds - } - return drawingRect - } -} - -// NIEUW: Custom Button klasse met icoon en subtiele tekst -class IconTextButton: NSButton { - - convenience init(sfSymbolName: String, title: String, target: AnyObject?, action: Selector?) { - self.init() - - self.target = target - self.action = action - - let symbolConfig = NSImage.SymbolConfiguration(pointSize: 12, weight: .regular) - if let iconImage = NSImage(systemSymbolName: sfSymbolName, accessibilityDescription: title)?.withSymbolConfiguration(symbolConfig) { - self.image = iconImage - } - - let paragraphStyle = NSMutableParagraphStyle() - paragraphStyle.alignment = .center - - let attributes: [NSAttributedString.Key: Any] = [ - .foregroundColor: NSColor(white: 0.90, alpha: 0.85), - .font: NSFont.systemFont(ofSize: 10, weight: .medium), - .paragraphStyle: paragraphStyle - ] - self.attributedTitle = NSAttributedString(string: title, attributes: attributes) - - self.setButtonType(.momentaryPushIn) - self.isBordered = false - self.imagePosition = .imageLeading - self.imageHugsTitle = true - self.alignment = .center - - self.wantsLayer = true - self.layer?.backgroundColor = NSColor.clear.cgColor - self.layer?.cornerRadius = 5 - - (self.cell as? NSButtonCell)?.setAccessibilityLabel(title) - } - - private var trackingArea: NSTrackingArea? - - override func updateTrackingAreas() { - super.updateTrackingAreas() - if let existingTrackingArea = self.trackingArea { - self.removeTrackingArea(existingTrackingArea) - } - let options: NSTrackingArea.Options = [.mouseEnteredAndExited, .activeAlways, .inVisibleRect] - trackingArea = NSTrackingArea(rect: self.bounds, options: options, owner: self, userInfo: nil) - if let ta = trackingArea { self.addTrackingArea(ta) } - } - - override func mouseEntered(with event: NSEvent) { - super.mouseEntered(with: event) - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.15 - context.allowsImplicitAnimation = true - self.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.15).cgColor - }, completionHandler: nil) - } - - override func mouseExited(with event: NSEvent) { - super.mouseExited(with: event) - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.2 - context.allowsImplicitAnimation = true - self.layer?.backgroundColor = NSColor.clear.cgColor - }, completionHandler: nil) - } - - override var intrinsicContentSize: NSSize { - var size = super.intrinsicContentSize - size.width += 16 // Extra padding: 8pt left, 8pt right - size.height = 24 - return size - } -} - -// Custom Panel for Rename Input -class RenamePromptPanel: NSPanel, NSTextFieldDelegate { - var textField: NSTextField! - var saveNameButton: IconTextButton! - var saveToFolderButton: IconTextButton! - var cancelButton: IconTextButton! - - var enteredName: String? { textField.stringValue } - let textFieldTag = 101 - - var onAction: ((NSApplication.ModalResponse) -> Void)? - var animationStartFrame: NSRect? - // private var keepKeyTimer: Timer? // Uitgecommentarieerd - - weak var actionHandlerDelegate: RenameActionHandlerDelegate? - - override var acceptsFirstResponder: Bool { - return true - } - override var canBecomeKey: Bool { true } - override var canBecomeMain: Bool { true } - - override func keyDown(with event: NSEvent) { - if event.keyCode == 53 { // ESC key - closeRenamePanelAndCleanup(response: .cancel) - return - } - super.keyDown(with: event) - } - - private func closeRenamePanelAndCleanup(response: NSApplication.ModalResponse) { - print("๐Ÿงผ DEBUG: closeRenamePanelAndCleanup called with response: \(response)") - // keepKeyTimer?.invalidate() // Uitgecommentarieerd - // keepKeyTimer = nil // Uitgecommentarieerd - - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.4 - context.timingFunction = CAMediaTimingFunction(name: .easeOut) - self.animator().alphaValue = 0 - if let startFrame = self.animationStartFrame { - let endFrame = NSRect(x: startFrame.midX - 25, y: startFrame.midY - 12.5, width: 50, height: 25) - self.animator().setFrame(endFrame, display: true) - } - }, completionHandler: { - self.orderOut(nil) - self.alphaValue = 1 - - self.actionHandlerDelegate?.enableGridMonitoring() - self.actionHandlerDelegate?.hideGrid() - - self.onAction?(response) - }) - } - - override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { - super.init(contentRect: contentRect, styleMask: [.borderless, .utilityWindow, .hudWindow, .nonactivatingPanel], backing: backingStoreType, defer: flag) - - self.isFloatingPanel = true - self.level = .floating + 3 // Teruggezet - self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] - self.isOpaque = false - self.backgroundColor = NSColor.clear - self.hasShadow = false - self.animationBehavior = .utilityWindow - self.becomesKeyOnlyIfNeeded = true - self.hidesOnDeactivate = false - - let visualEffectView = NSVisualEffectView() - visualEffectView.blendingMode = .behindWindow - visualEffectView.material = .hudWindow - visualEffectView.state = .active - visualEffectView.wantsLayer = true - visualEffectView.layer?.cornerRadius = 12.0 - visualEffectView.layer?.masksToBounds = true - visualEffectView.translatesAutoresizingMaskIntoConstraints = false - self.contentView = visualEffectView - - let horizontalPadding: CGFloat = 10 - let verticalPadding: CGFloat = 15 - let textFieldHeight: CGFloat = 19 - let spacingBelowTextField: CGFloat = 12 - let buttonSpacing: CGFloat = 8 - let buttonHeight: CGFloat = 24 - - textField = NSTextField() - textField.tag = textFieldTag - textField.delegate = self - textField.translatesAutoresizingMaskIntoConstraints = false - visualEffectView.addSubview(textField) - textFieldStyling() - - saveNameButton = IconTextButton(sfSymbolName: "checkmark.circle", title: " Save", target: self, action: #selector(buttonClicked(_:))) - saveNameButton.tag = NSApplication.ModalResponse.OK.rawValue - saveNameButton.translatesAutoresizingMaskIntoConstraints = false - visualEffectView.addSubview(saveNameButton) - - saveToFolderButton = IconTextButton(sfSymbolName: "folder.badge.plus", title: " Save", target: self, action: #selector(buttonClicked(_:))) - saveToFolderButton.tag = NSApplication.ModalResponse.continue.rawValue - saveToFolderButton.translatesAutoresizingMaskIntoConstraints = false - visualEffectView.addSubview(saveToFolderButton) - - cancelButton = IconTextButton(sfSymbolName: "xmark.circle", title: " Cancel", target: self, action: #selector(buttonClicked(_:))) - cancelButton.tag = NSApplication.ModalResponse.cancel.rawValue - cancelButton.translatesAutoresizingMaskIntoConstraints = false - visualEffectView.addSubview(cancelButton) - - let buttons = [saveNameButton!, saveToFolderButton!, cancelButton!] - let buttonStackView = NSStackView(views: buttons) - buttonStackView.orientation = .horizontal - buttonStackView.alignment = .centerY - buttonStackView.spacing = buttonSpacing - buttonStackView.distribution = .fillProportionally - buttonStackView.translatesAutoresizingMaskIntoConstraints = false - visualEffectView.addSubview(buttonStackView) - - let minPanelWidth: CGFloat = 280 - let calculatedPanelHeight = verticalPadding + textFieldHeight + spacingBelowTextField + buttonHeight + verticalPadding - self.setContentSize(NSSize(width: minPanelWidth, height: calculatedPanelHeight)) - - NSLayoutConstraint.activate([ - visualEffectView.leadingAnchor.constraint(equalTo: (self.contentView!).leadingAnchor), - visualEffectView.trailingAnchor.constraint(equalTo: (self.contentView!).trailingAnchor), - visualEffectView.topAnchor.constraint(equalTo: (self.contentView!).topAnchor), - visualEffectView.bottomAnchor.constraint(equalTo: (self.contentView!).bottomAnchor), - - textField.topAnchor.constraint(equalTo: visualEffectView.topAnchor, constant: verticalPadding), - textField.leadingAnchor.constraint(equalTo: visualEffectView.leadingAnchor, constant: horizontalPadding), - textField.trailingAnchor.constraint(equalTo: visualEffectView.trailingAnchor, constant: -horizontalPadding), - textField.heightAnchor.constraint(equalToConstant: textFieldHeight), - - buttonStackView.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: spacingBelowTextField), - buttonStackView.centerXAnchor.constraint(equalTo: visualEffectView.centerXAnchor), - buttonStackView.heightAnchor.constraint(equalToConstant: buttonHeight), - buttonStackView.leadingAnchor.constraint(greaterThanOrEqualTo: visualEffectView.leadingAnchor, constant: horizontalPadding), - buttonStackView.trailingAnchor.constraint(lessThanOrEqualTo: visualEffectView.trailingAnchor, constant: -horizontalPadding), - buttonStackView.bottomAnchor.constraint(equalTo: visualEffectView.bottomAnchor, constant: -verticalPadding) - ]) - - let stackViewWidthConstraint = buttonStackView.widthAnchor.constraint(lessThanOrEqualToConstant: minPanelWidth - 2 * horizontalPadding) - stackViewWidthConstraint.priority = .defaultHigh - stackViewWidthConstraint.isActive = true - - // keepKeyTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(ensureKeyWindow), userInfo: nil, repeats: true) // Uitgecommentarieerd - self.initialFirstResponder = textField - } - - private func textFieldStyling() { - textField.font = NSFont.systemFont(ofSize: 13) - textField.textColor = NSColor(white: 0.95, alpha: 1.0) - textField.isEditable = true - textField.isSelectable = true - textField.isBordered = false - textField.isBezeled = false - textField.backgroundColor = NSColor.clear - textField.focusRingType = .none - - textField.wantsLayer = true - textField.layer?.cornerRadius = 6.0 - textField.layer?.masksToBounds = true - textField.layer?.backgroundColor = NSColor.black.withAlphaComponent(0.25).cgColor - textField.layer?.borderColor = NSColor.gray.withAlphaComponent(0.4).cgColor - textField.layer?.borderWidth = 0.5 - - let cell = textField.cell as? NSTextFieldCell - cell?.usesSingleLineMode = true - cell?.wraps = false - cell?.isScrollable = true - cell?.isEditable = true - cell?.isSelectable = true - - // Set alignment AFTER configuring the cell - textField.alignment = .center - if let standardCell = textField.cell as? NSTextFieldCell { // Controleer of het een NSTextFieldCell is - standardCell.alignment = .center - } - } - - @objc func ensureKeyWindow() { - if !self.isKeyWindow { - self.makeKeyAndOrderFront(nil) - DispatchQueue.main.async { - _ = self.makeFirstResponder(self.textField) - } - } - } - - @objc func buttonClicked(_ sender: NSButton) { - let response = NSApplication.ModalResponse(rawValue: sender.tag) - print("๐Ÿ” DEBUG: RenamePromptPanel button clicked with tag: \(sender.tag), mapped to response: \(response)") - closeRenamePanelAndCleanup(response: response) - } - - func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { - if commandSelector == #selector(insertNewline(_:)) { - if let okButton = saveNameButton { - buttonClicked(okButton) - } - return true - } - return false - } - - -} - -protocol RenameActionHandlerDelegate: AnyObject { - func getScreenshotFolder() -> String? - func renameActionHandler(_ handler: RenameActionHandler, didRenameFileFrom oldURL: URL, to newURL: URL) - func findFilenameLabel(in window: NSWindow?) -> NSTextField? - func setTempFileURL(_ url: URL?) - func getActivePreviewWindow() -> NSWindow? - func closePreviewWithAnimation(immediate: Bool, preserveTempFile: Bool) - func getGridWindowFrame() -> NSRect? - func hideGrid() - func disableGridMonitoring() - func enableGridMonitoring() -} - -class RenameActionHandler { - weak var delegate: RenameActionHandlerDelegate? - private var renamePanel: RenamePromptPanel? - - init(delegate: RenameActionHandlerDelegate) { - self.delegate = delegate - } - - func isRenamePanelActive() -> Bool { - return renamePanel?.isVisible == true - } - - func closeRenamePanelAndCleanup() { - if let panel = self.renamePanel, panel.isVisible { - print("โ„น๏ธ Closing existing rename panel due to new action.") - - if let panelCancelButton = panel.cancelButton { - panel.buttonClicked(panelCancelButton) - } else { - print("โš ๏ธ Could not find cancel button in rename panel to simulate click.") - self.renamePanel = nil - } - } - } - - private func sanitize(name: String) -> String { - var characterSet = CharacterSet.alphanumerics - characterSet.insert(charactersIn: "-_") - let sanitized = name.trimmingCharacters(in: .whitespacesAndNewlines) - .components(separatedBy: characterSet.inverted).joined() - return sanitized.isEmpty ? "screenshot" : sanitized - } - - func promptAndRename(originalURL: URL, completion: @escaping (NSApplication.ModalResponse) -> Void) { - print("๐Ÿ” DEBUG: promptAndRename called with URL: \(originalURL)") - print("๐Ÿ” DEBUG: Current working directory: \(FileManager.default.currentDirectoryPath)") - print("๐Ÿ” DEBUG: Original URL exists: \(FileManager.default.fileExists(atPath: originalURL.path))") - - if let existingPanel = self.renamePanel, existingPanel.isVisible { - print("โ„น๏ธ Rename panel already visible. Bringing to front.") - existingPanel.makeKeyAndOrderFront(nil) - completion(.cancel) - return - } - - guard let handlerDelegate = self.delegate else { - print("โŒ RenameActionHandler: handlerDelegate (for self) not set.") - completion(.cancel) - return - } - - let tempFilename = originalURL.deletingPathExtension().lastPathComponent - - let gridFrame = handlerDelegate.getGridWindowFrame() - let panel = RenamePromptPanel(animationStartFrame: gridFrame) - - panel.actionHandlerDelegate = handlerDelegate - panel.textField.stringValue = tempFilename - self.renamePanel = panel - - panel.onAction = { [weak self] response in - guard let self = self else { - print("๐Ÿ” DEBUG: self is nil in onAction callback") - completion(.cancel) - return - } - - print("๐Ÿ” DEBUG: RenamePanel onAction called with response: \(response)") - let enteredName = self.renamePanel?.enteredName ?? "" - print("๐Ÿ” DEBUG: enteredName: '\(enteredName)'") - var actionResponse = response - - if response == .OK { - print("๐Ÿ” DEBUG: Processing Save Name Only action") - _ = self.performRename(originalURL: originalURL, newNameInput: enteredName) - } else if response == .continue { - print("๐Ÿ” DEBUG: Processing Save to Folder action") - if let renamedURL = self.performRename(originalURL: originalURL, newNameInput: enteredName) { - self.saveToDesignatedFolder(fileURL: renamedURL, desiredName: enteredName) - } else { - print("Rename failed or was cancelled, cannot proceed to Save to Folder.") - actionResponse = .cancel - } - } else { - print("๐Ÿ” DEBUG: Processing Cancel action or unknown response: \(response)") - } - - print("๐Ÿ” DEBUG: Calling completion handler with actionResponse: \(actionResponse)") - completion(actionResponse) - if response != .OK && response != .continue { - self.renamePanel = nil - } - } - - let previewFrame = handlerDelegate.getActivePreviewWindow()?.frame - let panelSize = panel.frame.size - - var finalFrame = panel.frame - - if let currentGridFrame = gridFrame { - let spacing: CGFloat = 20 - finalFrame.origin.x = currentGridFrame.origin.x - panelSize.width - spacing - finalFrame.origin.y = currentGridFrame.origin.y + (currentGridFrame.height - panelSize.height) / 2 - finalFrame.size = panelSize - - let targetScreenForGrid = NSScreen.screens.first { $0.frame.intersects(currentGridFrame) } ?? NSScreen.main ?? NSScreen.screens.first! - let screenVisibleFrame = targetScreenForGrid.visibleFrame - - if finalFrame.origin.x < screenVisibleFrame.origin.x { - finalFrame.origin.x = currentGridFrame.maxX + spacing - } - if finalFrame.origin.y < screenVisibleFrame.origin.y { - finalFrame.origin.y = screenVisibleFrame.origin.y + 50 - } - if finalFrame.maxY > screenVisibleFrame.maxY { - finalFrame.origin.y = screenVisibleFrame.maxY - panelSize.height - 50 - } - } else { - let previewScreen = handlerDelegate.getActivePreviewWindow()?.screen - let mouseScreen = NSScreen.screenWithMouse() - let targetScreenForFallback = previewScreen ?? mouseScreen ?? NSScreen.main ?? NSScreen.screens.first! - let screenVisibleFrame = targetScreenForFallback.visibleFrame - finalFrame.origin.x = screenVisibleFrame.origin.x + (screenVisibleFrame.width - panelSize.width) / 2 - finalFrame.origin.y = screenVisibleFrame.origin.y + (screenVisibleFrame.height - panelSize.height) / 2 - finalFrame.size = panelSize - } - - panel.alphaValue = 0.0 - let startFrameForAnimation: NSRect - - if let currentGridFrame = gridFrame { - startFrameForAnimation = NSRect(x: currentGridFrame.midX - panelSize.width / 2, - y: currentGridFrame.midY - panelSize.height / 2, - width: panelSize.width, - height: panelSize.height) - } else if let previewFrame = previewFrame { - startFrameForAnimation = NSRect(x: previewFrame.midX - panelSize.width / 2, - y: previewFrame.midY - panelSize.height / 2, - width: panelSize.width, - height: panelSize.height) - } else { - startFrameForAnimation = finalFrame.offsetBy(dx: 0, dy: 50) - } - panel.setFrame(startFrameForAnimation, display: false) - panel.animationStartFrame = startFrameForAnimation - - panel.orderFront(nil) - handlerDelegate.disableGridMonitoring() - - NSAnimationContext.runAnimationGroup({ - context in - context.duration = 0.3 - context.timingFunction = CAMediaTimingFunction(name: .easeOut) - panel.animator().alphaValue = 1.0 - panel.animator().setFrame(finalFrame, display: true) - }, completionHandler: { - print("โœจ Panel animation complete. Current panel alpha: \(panel.alphaValue), isVisible: \(panel.isVisible), frame: \(panel.frame)") - panel.makeKey() - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - if panel.isVisible { - if panel.alphaValue < 0.1 { - print("โš ๏ธ Panel is visible but alpha is very low. Might be a ghost panel.") - } - } else { - print("โš ๏ธ Panel disappeared unexpectedly after animation.") - self.renamePanel = nil - completion(.cancel) - } - } - - DispatchQueue.main.async { - if let field = panel.textField { - if panel.firstResponder != field { - if panel.makeFirstResponder(field) { - field.currentEditor()?.selectedRange = NSRange(location: 0, length: field.stringValue.count) - print("โŒจ๏ธ DEBUG: TextField is first responder, text selected.") - } else { - print("โš ๏ธ DEBUG: Failed to make TextField first responder.") - } - } else { - print("โŒจ๏ธ DEBUG: TextField was already first responder.") - field.currentEditor()?.selectedRange = NSRange(location: 0, length: field.stringValue.count) - } - } - } - }) - } - - private func performRename(originalURL: URL, newNameInput: String) -> URL? { - guard let handlerDelegateRef = delegate else { return nil } - - let currentNameWithoutExt = originalURL.deletingPathExtension().lastPathComponent - let sanitizedNewName = self.sanitize(name: newNameInput) - - if newNameInput.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || sanitizedNewName == currentNameWithoutExt.replacingOccurrences(of: "screenshot_", with: "").replacingOccurrences(of: ".", with: "_") { - print("โ„น๏ธ Name not changed or empty, using original: \(originalURL.lastPathComponent)") - handlerDelegateRef.setTempFileURL(originalURL) - DispatchQueue.main.async { - if let label = handlerDelegateRef.findFilenameLabel(in: handlerDelegateRef.getActivePreviewWindow()) { - label.stringValue = originalURL.lastPathComponent - label.toolTip = originalURL.lastPathComponent - } - } - return originalURL - } - - let fileManager = FileManager.default - let directory = originalURL.deletingLastPathComponent() - let newURL = directory.appendingPathComponent(sanitizedNewName).appendingPathExtension(originalURL.pathExtension.isEmpty ? "png" : originalURL.pathExtension) - - if fileManager.fileExists(atPath: newURL.path) { - print("โš ๏ธ Rename failed: File already exists at \(newURL.path)") - DispatchQueue.main.async { - let conflictAlert = NSAlert() - conflictAlert.messageText = "File Exists" - conflictAlert.informativeText = "A file named \"\(newURL.lastPathComponent)\" already exists. Please use a different name." - conflictAlert.addButton(withTitle: "OK") - if let panelWindow = self.renamePanel { - conflictAlert.beginSheetModal(for: panelWindow, completionHandler: nil) - } else { - conflictAlert.runModal() - } - } - return nil - } - - do { - try fileManager.moveItem(at: originalURL, to: newURL) - print("โœ… Temp File renamed from \(originalURL.lastPathComponent) to \(newURL.lastPathComponent)") - handlerDelegateRef.setTempFileURL(newURL) - DispatchQueue.main.async { - if let label = handlerDelegateRef.findFilenameLabel(in: handlerDelegateRef.getActivePreviewWindow()) { - label.stringValue = newURL.lastPathComponent - label.toolTip = newURL.lastPathComponent - } - } - return newURL - } catch { - print("โŒ Error renaming temp file: \(error)") - DispatchQueue.main.async { - let errorAlert = NSAlert(error: error) - if let panelWindow = self.renamePanel { - errorAlert.beginSheetModal(for: panelWindow, completionHandler: nil) - } else { - errorAlert.runModal() - } - } - return nil - } - } - - private func saveToDesignatedFolder(fileURL: URL, desiredName: String) { - guard let handlerDelegateRef = delegate else { return } - guard let destinationFolderStr = handlerDelegateRef.getScreenshotFolder(), - !destinationFolderStr.isEmpty else { - 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") - if let hostWindow = handlerDelegateRef.getActivePreviewWindow() ?? self.renamePanel { - alert.beginSheetModal(for: hostWindow, completionHandler: nil) - } else { - alert.runModal() - } - } - return - } - - let finalFilename = fileURL.lastPathComponent - - let fileManager = FileManager.default - let destinationFolderURL = URL(fileURLWithPath: destinationFolderStr) - let destinationURL = destinationFolderURL.appendingPathComponent(finalFilename) - - if fileManager.fileExists(atPath: destinationURL.path) { - print("โš ๏ธ Save failed: File already exists at \(destinationURL.path)") - DispatchQueue.main.async { - let conflictAlert = NSAlert() - conflictAlert.messageText = "File Exists in Destination" - conflictAlert.informativeText = "A file named \"\(destinationURL.lastPathComponent)\" already exists in the destination folder \"\(destinationFolderURL.lastPathComponent)\"." - conflictAlert.addButton(withTitle: "OK") - if let hostWindow = handlerDelegateRef.getActivePreviewWindow() ?? self.renamePanel { - conflictAlert.beginSheetModal(for: hostWindow, completionHandler: nil) - } else { - conflictAlert.runModal() - } - } - return - } - - do { - try fileManager.moveItem(at: fileURL, to: destinationURL) - print("โœ… Screenshot moved to: \(destinationURL.path)") - handlerDelegateRef.setTempFileURL(nil) - - DispatchQueue.main.async { - if !SettingsManager.shared.closeAfterSave { - print("RenameActionHandler: closeAfterSave is OFF, expliciet sluiten van preview na Save to Folder.") - handlerDelegateRef.closePreviewWithAnimation(immediate: false, preserveTempFile: false) - } - } - } catch { - print("โŒ Error moving file to destination folder: \(error)") - DispatchQueue.main.async { - let errorAlert = NSAlert(error: error) - if let hostWindow = handlerDelegateRef.getActivePreviewWindow() ?? self.renamePanel { - errorAlert.beginSheetModal(for: hostWindow, completionHandler: nil) - } else { - errorAlert.runModal() - } - } - } - } - - private func handleExistingFileConflict(for type: ConflictType, originalURL: URL, destinationURL: URL, destinationFolderURL: URL, completion: @escaping (NSApplication.ModalResponse) -> Void) { - let conflictAlert = NSAlert() - conflictAlert.messageText = "File Exists" - - let fileName = destinationURL.lastPathComponent - let folderName = destinationFolderURL.lastPathComponent - - if type == .renameOnly { - conflictAlert.informativeText = "A file named \"\(fileName)\" already exists. Please use a different name." - let okButton = conflictAlert.addButton(withTitle: "OK") - okButton.tag = NSApplication.ModalResponse.OK.rawValue - } else { - conflictAlert.informativeText = "A file named \"\(fileName)\" already exists in the destination folder \"\(folderName)\"." - let overwriteButton = conflictAlert.addButton(withTitle: "Overwrite") - overwriteButton.tag = NSApplication.ModalResponse.OK.rawValue - - let saveNewNameButton = conflictAlert.addButton(withTitle: "Save with New Name") - saveNewNameButton.tag = NSApplication.ModalResponse.continue.rawValue - - let cancelButton = conflictAlert.addButton(withTitle: "Cancel") - cancelButton.tag = NSApplication.ModalResponse.cancel.rawValue - } - - var response = conflictAlert.runModal() - - if type == .renameOnly && response == .OK { - response = .cancel - } - - completion(response) - } - - private func showRenameErrorAlert(message: String, informativeText: String, onOK: (() -> Void)? = nil) { - let alert = NSAlert() - alert.messageText = message - alert.informativeText = informativeText - let okButton = alert.addButton(withTitle: "OK") - okButton.tag = NSApplication.ModalResponse.OK.rawValue - - alert.beginSheetModal(for: self.renamePanel ?? NSApplication.shared.keyWindow ?? NSWindow()) { response in - if response == .OK { - onOK?() - } - } - } -} - -extension RenamePromptPanel { - convenience init(animationStartFrame: NSRect?) { - let initialWidth: CGFloat = 280 - let initialHeight: CGFloat = 130 - var initialRect = NSRect(x: 0, y: 0, width: initialWidth, height: initialHeight) - - if let startFrame = animationStartFrame, let mainScreen = NSScreen.main { - let screenFrame = mainScreen.visibleFrame - initialRect.origin.x = startFrame.midX - initialWidth / 2 - initialRect.origin.y = startFrame.midY - initialHeight / 2 - - if initialRect.maxX > screenFrame.maxX { initialRect.origin.x = screenFrame.maxX - initialWidth } - if initialRect.minX < screenFrame.minX { initialRect.origin.x = screenFrame.minX } - if initialRect.maxY > screenFrame.maxY { initialRect.origin.y = screenFrame.maxY - initialHeight } - if initialRect.minY < screenFrame.minY { initialRect.origin.y = screenFrame.minY } - } else if let mainScreen = NSScreen.main { - initialRect.origin.x = (mainScreen.visibleFrame.width - initialWidth) / 2 + mainScreen.visibleFrame.origin.x - initialRect.origin.y = (mainScreen.visibleFrame.height - initialHeight) / 2 + mainScreen.visibleFrame.origin.y - } - - self.init(contentRect: initialRect, styleMask: [.borderless, .utilityWindow, .hudWindow, .nonactivatingPanel], backing: .buffered, defer: false) - self.animationStartFrame = animationStartFrame - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/ScreenCaptureKitProvider.swift b/ShotScreen/Sources/ScreenCaptureKitProvider.swift deleted file mode 100644 index 0aa31ca..0000000 --- a/ShotScreen/Sources/ScreenCaptureKitProvider.swift +++ /dev/null @@ -1,318 +0,0 @@ -import ScreenCaptureKit -import AppKit - -@available(macOS 12.3, *) -class ScreenCaptureKitProvider { - - func getDesktopIconWindows() async -> [SCWindow] { - // Check if desktop icon hiding is enabled in settings - guard SettingsManager.shared.hideDesktopIconsDuringScreenshot else { - // If setting is disabled, return empty array so no windows are excluded - return [] - } - - do { - let content = try await SCShareableContent.current - let windows = content.windows - - let desktopIconWindows = windows.filter { window in - guard let app = window.owningApplication, app.bundleIdentifier == "com.apple.finder" else { - return false - } - guard window.title == nil || window.title == "" else { - return false - } - // Layer for desktop icons on Sonoma/Sequoia seems to be -2147483603 - // This value might be kCGDesktopWindowLevel - 20 (or similar) - // kCGDesktopWindowLevel is (CGWindowLevel) (kCGBaseWindowLevel + kCGDesktopWindowLevelKey) - // kCGBaseWindowLevel is Int32.min - // kCGDesktopWindowLevelKey is 20 - // So kCGDesktopWindowLevel is Int32.min + 20 = -2147483648 + 20 = -2147483628 - // The observed value -2147483603 might be kCGDesktopWindowLevel + 25 or similar constant. - // Let's stick to the observed value for now as it seems consistent. - return window.windowLayer == -2147483603 && window.isOnScreen && window.frame.size.width > 0 && window.frame.size.height > 0 - } - return desktopIconWindows - } catch { - NSLog("Error fetching shareable content for desktop icons: \(error.localizedDescription)") - return [] - } - } - - func getDesktopWidgetWindows() async -> [SCWindow] { - // Check if desktop widget hiding is enabled in settings - guard SettingsManager.shared.hideDesktopWidgetsDuringScreenshot else { - // If setting is disabled, return empty array so no windows are excluded - return [] - } - - do { - let content = try await SCShareableContent.current - let windows = content.windows - - // Use DesktopIconManager to detect widgets - let detectedWidgets = DesktopIconManager.shared.detectDesktopWidgets(from: windows) - NSLog("๐Ÿ” ScreenCaptureKitProvider: Found \(detectedWidgets.count) desktop widgets to hide") - return detectedWidgets - } catch { - NSLog("Error fetching shareable content for desktop widgets: \(error.localizedDescription)") - return [] - } - } - - func getAllWindowsToExclude() async -> [SCWindow] { - // Combine both desktop icons and widgets into one exclusion list - async let iconWindows = getDesktopIconWindows() - async let widgetWindows = getDesktopWidgetWindows() - - let allWindows = await iconWindows + widgetWindows - NSLog("๐Ÿ” ScreenCaptureKitProvider: Total windows to exclude: \(allWindows.count) (icons + widgets)") - return allWindows - } - - func captureScreen(screen: NSScreen, excludingWindows: [SCWindow]? = nil) async -> NSImage? { - do { - let content = try await SCShareableContent.current - guard let display = content.displays.first(where: { $0.displayID == screen.displayID }) else { - NSLog("Error: Could not find SCDisplay matching NSScreen with ID: \(screen.displayID)") - return nil - } - - var windowsToExclude = excludingWindows ?? [] - if let ownBundleID = Bundle.main.bundleIdentifier { - let ownWindows = content.windows.filter { $0.owningApplication?.bundleIdentifier == ownBundleID && $0.isOnScreen } - windowsToExclude.append(contentsOf: ownWindows) - } - - let filter = SCContentFilter(display: display, excludingWindows: windowsToExclude) - let configuration = SCStreamConfiguration() - - configuration.width = display.width - configuration.height = display.height - configuration.showsCursor = SettingsManager.shared.windowCaptureIncludeCursor - configuration.capturesAudio = false - configuration.pixelFormat = kCVPixelFormatType_32BGRA - configuration.colorSpaceName = CGColorSpace.sRGB - - let stream = SCStream(filter: filter, configuration: configuration, delegate: nil) - try await stream.addStreamOutput(SingleFrameOutput.shared, type: .screen, sampleHandlerQueue: .main) - try await stream.startCapture() - // Wacht kort op een frame - dit is een placeholder, een betere synchronisatie is nodig - try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconde - let image = await SingleFrameOutput.shared.retrieveFrame() - try await stream.stopCapture() - return image - } catch { - NSLog("Error capturing screen with SCStream: \(error.localizedDescription)") - return nil - } - } - - func captureSelection(selectionRectInPoints: CGRect, screen: NSScreen, excludingWindows: [SCWindow]? = nil) async -> NSImage? { - do { - let content = try await SCShareableContent.current - guard let display = content.displays.first(where: { $0.displayID == screen.displayID }) else { - NSLog("Error: Could not find SCDisplay matching NSScreen with ID: \(screen.displayID) for selection capture") - return nil - } - - let scale = screen.backingScaleFactor - - // selectionRectInPoints is de intersectie van de globale selectie met screen.frame, - // nog steeds in globale AppKit coรถrdinaten (Y-omhoog, oorsprong linksonder hoofdvenster). - - // Stap 1: Converteer globale AppKit selectie naar lokale AppKit punten voor dit scherm. - // (Y-omhoog, oorsprong linksonder DIT scherm). - let localOriginXInPoints = selectionRectInPoints.origin.x - screen.frame.origin.x - let localOriginYInPoints = selectionRectInPoints.origin.y - screen.frame.origin.y - // Breedte en hoogte (in punten) blijven hetzelfde als selectionRectInPoints.size.width/height - - // Stap 2: Converteer naar sourceRect in fysieke pixels, Y-omlaag, oorsprong linksboven DIT scherm. - // AANNAMES GEWIJZIGD: selectionRectInPoints en screen.frame lijken al in de 'pixel'-eenheid te zijn - // die SCDisplay verwacht, ondanks dat ze 'punten' worden genoemd. De 'scale' factor wordt hier dus niet gebruikt voor de conversie. - let sourceRectXPixels = localOriginXInPoints - let sourceRectYPixels = (screen.frame.size.height - (localOriginYInPoints + selectionRectInPoints.size.height)) - let sourceRectWidthPixels = selectionRectInPoints.size.width - let sourceRectHeightPixels = selectionRectInPoints.size.height - - var sourceRect = CGRect( - x: sourceRectXPixels, - y: sourceRectYPixels, - width: sourceRectWidthPixels, - height: sourceRectHeightPixels - ) - - // Rond af naar dichtstbijzijnde gehele pixel om mogelijke SCK API problemen te voorkomen - sourceRect = CGRect( - x: round(sourceRect.origin.x), - y: round(sourceRect.origin.y), - width: round(sourceRect.size.width), - height: round(sourceRect.size.height) - ) - - NSLog("๐ŸŽฏ CAPTURE SELECTION DEBUG V3 (Simplified V1 Logic):") - NSLog(" Input selectionRectInPoints (AppKit Global Y-up): \(selectionRectInPoints)") - NSLog(" Target NSScreen: \(screen.customLocalizedName), Frame (AppKit Y-up): \(screen.frame), Scale: \(scale)") - NSLog(" Calculated localOriginInPoints (AppKit Y-up, screen-local): x=\(localOriginXInPoints), y=\(localOriginYInPoints)") - NSLog(" SCDisplay: ID \(display.displayID), display.width (pixels): \(display.width), display.height (pixels): \(display.height)") - NSLog(" Calculated sourceRect (Physical Pixels, screen-local, Y-down from screen top-left, rounded): \(sourceRect)") - - // Basis validatie - guard sourceRect.width > 0 && sourceRect.height > 0 else { - NSLog("Error V3: Calculated sourceRect has zero or negative rounded width/height. sourceRect: \(sourceRect)") - return nil - } - - // Strikte grenscontrole en eventuele clipping - if !(sourceRect.origin.x >= 0 && - sourceRect.origin.y >= 0 && - sourceRect.maxX <= CGFloat(display.width) + 0.5 && - sourceRect.maxY <= CGFloat(display.height) + 0.5) { - - NSLog("Warning V3: Calculated sourceRect \(sourceRect) is out of bounds for the SCDisplay [W:\(display.width), H:\(display.height)]. Attempting to clip.") - // Log de individuele checks voor duidelijkheid - NSLog(" Check: sourceRect.origin.x (\(sourceRect.origin.x)) >= 0") - NSLog(" Check: sourceRect.origin.y (\(sourceRect.origin.y)) >= 0") - NSLog(" Check: sourceRect.maxX (\(sourceRect.maxX)) <= display.width (\(CGFloat(display.width) + 0.5))") - NSLog(" Check: sourceRect.maxY (\(sourceRect.maxY)) <= display.height (\(CGFloat(display.height) + 0.5))") - - let clippedRect = sourceRect.intersection(CGRect(x: 0, y: 0, width: CGFloat(display.width), height: CGFloat(display.height))) - - if clippedRect.width > 1 && clippedRect.height > 1 { - NSLog(" Successfully clipped sourceRect to: \(clippedRect)") - sourceRect = clippedRect - } else { - NSLog("Error V3: Clipping failed or resulted in too small rect: \(clippedRect). Aborting.") - return nil - } - } - - var windowsToExclude = excludingWindows ?? [] - if let ownBundleID = Bundle.main.bundleIdentifier { - let ownWindows = content.windows.filter { $0.owningApplication?.bundleIdentifier == ownBundleID && $0.isOnScreen } - windowsToExclude.append(contentsOf: ownWindows) - } - - let filter = SCContentFilter(display: display, excludingWindows: windowsToExclude) - let configuration = SCStreamConfiguration() - - configuration.sourceRect = sourceRect - configuration.width = Int(sourceRect.width) - configuration.height = Int(sourceRect.height) - configuration.showsCursor = SettingsManager.shared.windowCaptureIncludeCursor - configuration.capturesAudio = false - configuration.pixelFormat = kCVPixelFormatType_32BGRA - configuration.colorSpaceName = CGColorSpace.sRGB - - let stream = SCStream(filter: filter, configuration: configuration, delegate: nil) - try await stream.addStreamOutput(SingleFrameOutput.shared, type: .screen, sampleHandlerQueue: .main) - try await stream.startCapture() - try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconde - let image = await SingleFrameOutput.shared.retrieveFrame() - try await stream.stopCapture() - return image - } catch { - NSLog("Error capturing selection with SCStream: \(error.localizedDescription)") - return nil - } - } - - func captureWindow(window: SCWindow) async -> NSImage? { - // Ensure the window is on screen and has a valid frame. - // SCWindow.frame is in SCK global coordinates (Y-down, origin top-left of main display usually). - // We need to ensure it has a non-zero size. - guard window.isOnScreen, window.frame.width > 0, window.frame.height > 0 else { - NSLog("Error: Window to capture is not on screen or has invalid frame: \(window.windowID), Title: \(window.title ?? "N/A"), Frame: \(window.frame)") - return nil - } - - // For capturing a single window, we don't need to exclude other windows explicitly in the filter, - // as the filter will be configured to only include this specific window. - // However, we DO need to exclude our own app's overlay windows if they happen to be on top of the target window. - var windowsToExclude: [SCWindow] = [] - if let ownBundleID = Bundle.main.bundleIdentifier { - do { - let content = try await SCShareableContent.current - let ownWindows = content.windows.filter { $0.owningApplication?.bundleIdentifier == ownBundleID && $0.isOnScreen && $0.windowID != window.windowID } - windowsToExclude.append(contentsOf: ownWindows) - } catch { - NSLog("Error fetching shareable content for own window exclusion: \(error.localizedDescription)") - } - } - - let filter = SCContentFilter(desktopIndependentWindow: window) - let configuration = SCStreamConfiguration() - - // The frame of SCWindow is already in pixels (SCK coordinates). - // The width and height should be set to the window's frame size. - configuration.width = Int(window.frame.width) - configuration.height = Int(window.frame.height) - configuration.showsCursor = SettingsManager.shared.windowCaptureIncludeCursor - configuration.capturesAudio = false - configuration.pixelFormat = kCVPixelFormatType_32BGRA - // For window capture, SCContentFilter is configured with a single window, so sourceRect is not needed. - // The scaleFactor and pointPixelConversion properties on SCWindow might be useful if further coordinate transformations were needed, - // but captureImage with a window filter typically handles this. - - do { - let stream = SCStream(filter: filter, configuration: configuration, delegate: nil) - try await stream.addStreamOutput(SingleFrameOutput.shared, type: .screen, sampleHandlerQueue: .main) - try await stream.startCapture() - try await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconde - let image = await SingleFrameOutput.shared.retrieveFrame() - try await stream.stopCapture() - return image - } catch { - NSLog("Error capturing window ID \(window.windowID) with SCStream: \(error.localizedDescription)") - return nil - } - } - - // We will add other screenshot methods here later -} - -// Helper class to capture a single frame from SCStream -@MainActor -class SingleFrameOutput: NSObject, SCStreamOutput { - static let shared = SingleFrameOutput() - private var capturedImage: NSImage? - private var continuation: CheckedContinuation? - - // MOET NONISOLATED ZIJN VANWEGE PROTOCOL - nonisolated func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) { - // Moet terug naar MainActor voor UI updates/property access - Task { @MainActor in - guard type == .screen, CMSampleBufferIsValid(sampleBuffer), CMSampleBufferGetNumSamples(sampleBuffer) == 1 else { - return - } - - guard let cvPixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { - return - } - - let ciImage = CIImage(cvPixelBuffer: cvPixelBuffer) - let rep = NSCIImageRep(ciImage: ciImage) - let nsImage = NSImage(size: rep.size) - nsImage.addRepresentation(rep) - - self.capturedImage = nsImage - self.continuation?.resume(returning: nsImage) - self.continuation = nil - } - } - - // Deze functie wordt aangeroepen vanaf een andere actor (degene die capture aanroept) - // maar interacteert met @MainActor properties via de continuation. - func retrieveFrame() async -> NSImage? { - if let image = capturedImage { // Lees direct als al beschikbaar (op MainActor) - self.capturedImage = nil - return image - } - return await withCheckedContinuation { continuation in - // De continuation zelf is Sendable. - // De .resume() wordt aangeroepen vanuit de (nonisolated) stream functie, - // maar de Task daarbinnen springt terug naar @MainActor voor de daadwerkelijke resume. - self.continuation = continuation - } - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/SettingsManager.swift b/ShotScreen/Sources/SettingsManager.swift deleted file mode 100644 index 1ebd4f9..0000000 --- a/ShotScreen/Sources/SettingsManager.swift +++ /dev/null @@ -1,962 +0,0 @@ -import AppKit -import SwiftUI // Needed for ObservableObject, Published, etc. -import Combine // Needed for objectWillChange - -class SettingsManager: ObservableObject { - static let shared = SettingsManager() - private var isInitializing = false // NIEUWE FLAG - - var screenshotFolder: String? { - get { UserDefaults.standard.string(forKey: SettingsKey.screenshotFolder) } - set { - objectWillChange.send() - UserDefaults.standard.set(newValue, forKey: SettingsKey.screenshotFolder) - } - } - - var thumbnailTimer: Int { - get { UserDefaults.standard.integer(forKey: SettingsKey.thumbnailTimer) } - set { objectWillChange.send(); UserDefaults.standard.set(newValue, forKey: SettingsKey.thumbnailTimer) } - } - - @Published var closeAfterDrag: Bool = false { - didSet { - UserDefaults.standard.set(closeAfterDrag, forKey: SettingsKey.closeAfterDrag) - if !isInitializing { - NotificationCenter.default.post(name: .closeAfterDragSettingChanged, object: nil) - } - } - } - - var showFolderButton: Bool { - get { UserDefaults.standard.bool(forKey: SettingsKey.showFolderButton) } - set { objectWillChange.send(); UserDefaults.standard.set(newValue, forKey: SettingsKey.showFolderButton) } - } - - @Published var closeAfterSave: Bool = false { - didSet { - UserDefaults.standard.set(closeAfterSave, forKey: SettingsKey.closeAfterSave) - if !isInitializing { - NotificationCenter.default.post(name: .closeAfterSaveSettingChanged, object: nil) - } - } - } - - @Published var playSoundOnCapture: Bool = true { - didSet { - UserDefaults.standard.set(playSoundOnCapture, forKey: SettingsKey.playSoundOnCapture) - if !isInitializing { // CONTROLEER FLAG - NotificationCenter.default.post(name: .playSoundOnCaptureSettingChanged, object: nil) - } - } - } - - // NEW: Properties for filename format - var filenamePrefix: String { - get { UserDefaults.standard.string(forKey: SettingsKey.filenamePrefix) ?? "Schermafbeelding" } // Default prefix - set { objectWillChange.send(); UserDefaults.standard.set(newValue, forKey: SettingsKey.filenamePrefix) } - } - - var filenameFormatPreset: FilenameFormatPreset { - get { - let rawValue = UserDefaults.standard.integer(forKey: SettingsKey.filenameFormatPreset) - return FilenameFormatPreset(rawValue: rawValue) ?? .macOSStyle // Default naar macOS style - } - set { objectWillChange.send(); UserDefaults.standard.set(newValue.rawValue, forKey: SettingsKey.filenameFormatPreset) } - } - - var filenameCustomFormat: String { - get { UserDefaults.standard.string(forKey: SettingsKey.filenameCustomFormat) ?? "{YYYY}-{MM}-{DD}_{hh}.{mm}.{ss}" } // Default custom format - set { objectWillChange.send(); UserDefaults.standard.set(newValue, forKey: SettingsKey.filenameCustomFormat) } - } - - // NEW: Properties for action enables - var isRenameActionEnabled: Bool { - get { - let value = UserDefaults.standard.object(forKey: SettingsKey.isRenameActionEnabled) as? Bool ?? true - return value - } - set { - objectWillChange.send() - UserDefaults.standard.set(newValue, forKey: SettingsKey.isRenameActionEnabled) - } - } - var isStashActionEnabled: Bool { - get { - let value = UserDefaults.standard.object(forKey: SettingsKey.isStashActionEnabled) as? Bool ?? true - return value - } - set { - objectWillChange.send() - UserDefaults.standard.set(newValue, forKey: SettingsKey.isStashActionEnabled) - } - } - var isOCRActionEnabled: Bool { - get { - let value = UserDefaults.standard.object(forKey: SettingsKey.isOCRActionEnabled) as? Bool ?? true - return value - } - set { - objectWillChange.send() - UserDefaults.standard.set(newValue, forKey: SettingsKey.isOCRActionEnabled) - } - } - var isClipboardActionEnabled: Bool { - get { - let value = UserDefaults.standard.object(forKey: SettingsKey.isClipboardActionEnabled) as? Bool ?? true - return value - } - set { - objectWillChange.send() - UserDefaults.standard.set(newValue, forKey: SettingsKey.isClipboardActionEnabled) - } - } - var isBackgroundRemoveActionEnabled: Bool { - get { - let value = UserDefaults.standard.object(forKey: SettingsKey.isBackgroundRemoveActionEnabled) as? Bool ?? true - return value - } - set { - objectWillChange.send() - UserDefaults.standard.set(newValue, forKey: SettingsKey.isBackgroundRemoveActionEnabled) - } - } - var isCancelActionEnabled: Bool { - get { - let value = UserDefaults.standard.object(forKey: SettingsKey.isCancelActionEnabled) as? Bool ?? true - return value - } - set { - objectWillChange.send() - UserDefaults.standard.set(newValue, forKey: SettingsKey.isCancelActionEnabled) - } - } - var isRemoveActionEnabled: Bool { - get { - let value = UserDefaults.standard.object(forKey: SettingsKey.isRemoveActionEnabled) as? Bool ?? true - return value - } - set { - objectWillChange.send() - UserDefaults.standard.set(newValue, forKey: SettingsKey.isRemoveActionEnabled) - } - } - var isAction3Enabled: Bool { - get { - let value = UserDefaults.standard.object(forKey: SettingsKey.isAction3Enabled) as? Bool ?? false - return value - } - set { - objectWillChange.send() - UserDefaults.standard.set(newValue, forKey: SettingsKey.isAction3Enabled) - } - } - var isAction4Enabled: Bool { - get { - let value = UserDefaults.standard.object(forKey: SettingsKey.isAction4Enabled) as? Bool ?? false - return value - } - set { - objectWillChange.send() - UserDefaults.standard.set(newValue, forKey: SettingsKey.isAction4Enabled) - } - } - - // NEW: Properties for Delete and Cancel Drag actions - var isDeleteActionEnabled: Bool { - get { UserDefaults.standard.object(forKey: SettingsKey.isDeleteActionEnabled) as? Bool ?? true } // Default AAN - set { objectWillChange.send(); UserDefaults.standard.set(newValue, forKey: SettingsKey.isDeleteActionEnabled) } - } - var isCancelDragActionEnabled: Bool { - get { UserDefaults.standard.object(forKey: SettingsKey.isCancelDragActionEnabled) as? Bool ?? true } // Default AAN - set { objectWillChange.send(); UserDefaults.standard.set(newValue, forKey: SettingsKey.isCancelDragActionEnabled) } - } - - // ๐ŸŽจ Background Removal Method Preference - var preferredBackgroundRemovalMethod: BackgroundRemovalMethod { - get { - let rawValue = UserDefaults.standard.string(forKey: SettingsKey.preferredBackgroundRemovalMethod) ?? "auto" - return BackgroundRemovalMethod(rawValue: rawValue) ?? .auto - } - set { - objectWillChange.send() - UserDefaults.standard.set(newValue.rawValue, forKey: SettingsKey.preferredBackgroundRemovalMethod) - } - } - - // NEW: Property for automatic startup - @Published var startAppOnLogin: Bool = false { - didSet { - UserDefaults.standard.set(startAppOnLogin, forKey: SettingsKey.startAppOnLogin) - if !isInitializing { - NotificationCenter.default.post(name: .startAppOnLoginSettingChanged, object: nil) - } - } - } - - // NEW: Property for automatic screenshot saving - var autoSaveScreenshot: Bool { - get { UserDefaults.standard.object(forKey: SettingsKey.autoSaveScreenshot) as? Bool ?? false } // Default false (handmatig beslissen) - set { - objectWillChange.send() - UserDefaults.standard.set(newValue, forKey: SettingsKey.autoSaveScreenshot) - // Optioneel: post een notificatie als directe actie in ScreenshotApp nodig is - // NotificationCenter.default.post(name: .autoSaveScreenshotChanged, object: nil) - } - } - - // VOEG DEZE TOE - static let thumbnailFixedSizeKey = "thumbnailFixedSize" - @Published var thumbnailFixedSize: ThumbnailFixedSize = .medium { - didSet { - UserDefaults.standard.set(thumbnailFixedSize.rawValue, forKey: SettingsManager.thumbnailFixedSizeKey) - if !isInitializing { - NotificationCenter.default.post(name: .thumbnailSizeSettingChanged, object: nil) - } - } - } - - // NEW: Property for stash always on top - @Published var stashAlwaysOnTop: Bool = false { - didSet { - UserDefaults.standard.set(stashAlwaysOnTop, forKey: SettingsKey.stashAlwaysOnTop) - if !isInitializing { - NotificationCenter.default.post(name: .stashAlwaysOnTopSettingChanged, object: nil) - } - } - } - - // ๐Ÿ”ฅ๐Ÿ’Ž MEGA NIEUWE STASH PREVIEW SIZE SETTING! ๐Ÿ’Ž๐Ÿ”ฅ - @Published var stashPreviewSize: StashPreviewSize = .medium { - didSet { - UserDefaults.standard.set(stashPreviewSize.rawValue, forKey: SettingsKey.stashPreviewSize) - if !isInitializing { - NotificationCenter.default.post(name: .stashPreviewSizeChanged, object: nil) - } - } - } - - // ๐Ÿ”ฅ๐Ÿ’ฅโšก HYPERMODE STASH GRID CONFIGURATION! โšก๐Ÿ’ฅ๐Ÿ”ฅ - @Published var stashGridMode: StashGridMode = .fixedColumns { - didSet { - UserDefaults.standard.set(stashGridMode.rawValue, forKey: SettingsKey.stashGridMode) - if !isInitializing { - NotificationCenter.default.post(name: .stashGridModeChanged, object: nil) - NotificationCenter.default.post(name: .stashGridConfigChanged, object: nil) - } - } - } - - @Published var stashMaxColumns: Int = 2 { - didSet { - // Clamp to 1-5 range for HYPERMODE safety! - let clampedValue = max(1, min(5, stashMaxColumns)) - if clampedValue != stashMaxColumns { - stashMaxColumns = clampedValue - return - } - UserDefaults.standard.set(stashMaxColumns, forKey: SettingsKey.stashMaxColumns) - if !isInitializing { - NotificationCenter.default.post(name: .stashGridConfigChanged, object: nil) - } - } - } - - @Published var stashMaxRows: Int = 1 { - didSet { - // Clamp to 1-5 range for HYPERMODE safety! - let clampedValue = max(1, min(5, stashMaxRows)) - if clampedValue != stashMaxRows { - stashMaxRows = clampedValue - return - } - UserDefaults.standard.set(stashMaxRows, forKey: SettingsKey.stashMaxRows) - if !isInitializing { - NotificationCenter.default.post(name: .stashGridConfigChanged, object: nil) - } - } - } - - // ๐Ÿ”ฅ NIEUW: Persistent stash setting - @Published var persistentStash: Bool = false { - didSet { - UserDefaults.standard.set(persistentStash, forKey: SettingsKey.persistentStash) - if !isInitializing { - NotificationCenter.default.post(name: .persistentStashChanged, object: nil) - } - } - } - - // ๐Ÿ”„ UPDATE SETTINGS - @Published var automaticUpdates: Bool = true { - didSet { - UserDefaults.standard.set(automaticUpdates, forKey: SettingsKey.automaticUpdates) - } - } - - @Published var includePreReleases: Bool = false { - didSet { - UserDefaults.standard.set(includePreReleases, forKey: SettingsKey.includePreReleases) - } - } - - // NEW: Property for hiding desktop icons during screenshots - @Published var hideDesktopIconsDuringScreenshot: Bool = false { - didSet { - UserDefaults.standard.set(hideDesktopIconsDuringScreenshot, forKey: SettingsKey.hideDesktopIconsDuringScreenshot) - if !isInitializing { - NotificationCenter.default.post(name: .hideDesktopIconsSettingChanged, object: nil) - } - print("Setting updated: hideDesktopIconsDuringScreenshot = \(hideDesktopIconsDuringScreenshot)") - } - } - - // NEW: Property for hiding desktop widgets during screenshots - @Published var hideDesktopWidgetsDuringScreenshot: Bool = false { - didSet { - UserDefaults.standard.set(hideDesktopWidgetsDuringScreenshot, forKey: SettingsKey.hideDesktopWidgetsDuringScreenshot) - if !isInitializing { - NotificationCenter.default.post(name: .hideDesktopWidgetsSettingChanged, object: nil) - } - print("Setting updated: hideDesktopWidgetsDuringScreenshot = \(hideDesktopWidgetsDuringScreenshot)") - } - } - - // ๐Ÿ”Š NEW: Sound volume and type settings - @Published var screenshotSoundVolume: Float = 0.1 { - didSet { - UserDefaults.standard.set(screenshotSoundVolume, forKey: SettingsKey.screenshotSoundVolume) - } - } - - @Published var screenshotSoundType: ScreenshotSoundType = .pop { - didSet { - if let data = try? JSONEncoder().encode(screenshotSoundType) { - UserDefaults.standard.set(data, forKey: SettingsKey.screenshotSoundType) - } - } - } - - // ๐Ÿ—‚๏ธ NEW: Cache management settings - @Published var cacheRetentionTime: CacheRetentionTime = .oneWeek { - didSet { - if let data = try? JSONEncoder().encode(cacheRetentionTime) { - UserDefaults.standard.set(data, forKey: SettingsKey.cacheRetentionTime) - } - // ๐Ÿงช NIEUW: Send notification for cache retention time changes (except during initialization) - if !isInitializing { - NotificationCenter.default.post(name: NSNotification.Name("cacheRetentionTimeChanged"), object: nil) - } - } - } - - - - // NEW: Property for first launch completion - var hasCompletedFirstLaunch: Bool { - get { UserDefaults.standard.bool(forKey: SettingsKey.hasCompletedFirstLaunch) } - set { - objectWillChange.send() - UserDefaults.standard.set(newValue, forKey: SettingsKey.hasCompletedFirstLaunch) - } - } - - var windowCaptureIncludeCursor: Bool { - get { UserDefaults.standard.bool(forKey: SettingsKey.windowCaptureIncludeCursor) } - set { - objectWillChange.send() - UserDefaults.standard.set(newValue, forKey: SettingsKey.windowCaptureIncludeCursor) - if !isInitializing { - NotificationCenter.default.post(name: .windowCaptureSettingChanged, object: nil) - } - } - } - - // NEW: Property for clean desktop screenshots - // VERWIJDERD: cleanDesktopScreenshots property - feature disabled - - @Published var saveAfterEdit: Bool = false { - didSet { - UserDefaults.standard.set(saveAfterEdit, forKey: SettingsKey.saveAfterEdit) - NotificationCenter.default.post(name: .saveAfterEditSettingChanged, object: nil) - } - } - - var thumbnailDisplayScreen: ThumbnailDisplayScreen { - get { - let rawValue = UserDefaults.standard.string(forKey: SettingsKey.thumbnailDisplayScreen) ?? ThumbnailDisplayScreen.automatic.rawValue - return ThumbnailDisplayScreen(rawValue: rawValue) ?? .automatic - } - set { - objectWillChange.send() - UserDefaults.standard.set(newValue.rawValue, forKey: SettingsKey.thumbnailDisplayScreen) - } - } - - static let actionOrderKey = "actionOrder" - @Published var actionOrder: [ActionType] = [] { - didSet { - let orderStrings = actionOrder.map { $0.rawValue } - UserDefaults.standard.set(orderStrings, forKey: SettingsManager.actionOrderKey) - } - } - - // NEW: Keyboard shortcut properties - @Published var useCustomShortcut: Bool = false { - didSet { - UserDefaults.standard.set(useCustomShortcut, forKey: SettingsKey.useCustomShortcut) - if !isInitializing { - // Post notification for hotkey change - NotificationCenter.default.post(name: .shortcutSettingChanged, object: nil) - } - } - } - - @Published var customShortcutModifiers: UInt = 0 { - didSet { - UserDefaults.standard.set(customShortcutModifiers, forKey: SettingsKey.customShortcutModifiers) - if !isInitializing && useCustomShortcut { - NotificationCenter.default.post(name: .shortcutSettingChanged, object: nil) - } - } - } - - @Published var customShortcutKey: UInt16 = 0 { - didSet { - UserDefaults.standard.set(customShortcutKey, forKey: SettingsKey.customShortcutKey) - if !isInitializing && useCustomShortcut { - NotificationCenter.default.post(name: .shortcutSettingChanged, object: nil) - } - } - } - - - - private init() { - isInitializing = true // ZET FLAG - - // Laad ALLE properties hier uit UserDefaults - self.screenshotFolder = UserDefaults.standard.string(forKey: SettingsKey.screenshotFolder) // Kan nil zijn - self.filenamePrefix = UserDefaults.standard.string(forKey: SettingsKey.filenamePrefix) ?? "Schermafbeelding" - - let presetRaw = UserDefaults.standard.integer(forKey: SettingsKey.filenameFormatPreset) - self.filenameFormatPreset = FilenameFormatPreset(rawValue: presetRaw) ?? .macOSStyle - // Als de opgeslagen rawValue 0 was en .macOSStyle is 0, dan is dit onnodig dubbel. - // Overweeg: if UserDefaults.standard.object(forKey: SettingsKey.filenameFormatPreset) != nil { ... laden ... } else { default } - - self.filenameCustomFormat = UserDefaults.standard.string(forKey: SettingsKey.filenameCustomFormat) ?? "{YYYY}-{MM}-{DD}_{hh}.{mm}.{ss}" - self.saveAfterEdit = UserDefaults.standard.bool(forKey: SettingsKey.saveAfterEdit) // Default false als key niet bestaat - self.playSoundOnCapture = UserDefaults.standard.object(forKey: SettingsKey.playSoundOnCapture) as? Bool ?? true - self.thumbnailTimer = UserDefaults.standard.integer(forKey: SettingsKey.thumbnailTimer) - self.closeAfterDrag = UserDefaults.standard.bool(forKey: SettingsKey.closeAfterDrag) - - let thumbFixedSizeKeyString = SettingsManager.thumbnailFixedSizeKey - let loadedFixedSizeRawValue = UserDefaults.standard.string(forKey: thumbFixedSizeKeyString) - if let loadedSize = loadedFixedSizeRawValue.flatMap(ThumbnailFixedSize.init) { - if self.thumbnailFixedSize != loadedSize { - self.thumbnailFixedSize = loadedSize - } - } else { - UserDefaults.standard.set(ThumbnailFixedSize.medium.rawValue, forKey: thumbFixedSizeKeyString) - } - - self.showFolderButton = UserDefaults.standard.object(forKey: SettingsKey.showFolderButton) as? Bool ?? true - self.startAppOnLogin = UserDefaults.standard.object(forKey: SettingsKey.startAppOnLogin) as? Bool ?? false - self.autoSaveScreenshot = UserDefaults.standard.object(forKey: SettingsKey.autoSaveScreenshot) as? Bool ?? false - self.closeAfterSave = UserDefaults.standard.bool(forKey: SettingsKey.closeAfterSave) - - // Stash window border etc. ook hier initialiseren: - // self.stashWindowBorderWidth = UserDefaults.standard.object(forKey: SettingsKey.stashWindowBorderWidth) == nil ? 1.0 : CGFloat(UserDefaults.standard.float(forKey: SettingsKey.stashWindowBorderWidth)) - self.isRenameActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isRenameActionEnabled) as? Bool ?? true - self.isStashActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isStashActionEnabled) as? Bool ?? true - self.isOCRActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isOCRActionEnabled) as? Bool ?? true - self.isClipboardActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isClipboardActionEnabled) as? Bool ?? true - self.isBackgroundRemoveActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isBackgroundRemoveActionEnabled) as? Bool ?? true - self.isCancelActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isCancelActionEnabled) as? Bool ?? true - self.isRemoveActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isRemoveActionEnabled) as? Bool ?? true - self.isDeleteActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isDeleteActionEnabled) as? Bool ?? true - self.isCancelDragActionEnabled = UserDefaults.standard.object(forKey: SettingsKey.isCancelDragActionEnabled) as? Bool ?? true - self.stashAlwaysOnTop = UserDefaults.standard.object(forKey: SettingsKey.stashAlwaysOnTop) as? Bool ?? false - self.hideDesktopIconsDuringScreenshot = UserDefaults.standard.object(forKey: SettingsKey.hideDesktopIconsDuringScreenshot) as? Bool ?? false - self.hideDesktopWidgetsDuringScreenshot = UserDefaults.standard.object(forKey: SettingsKey.hideDesktopWidgetsDuringScreenshot) as? Bool ?? false - - - // ๐Ÿ”ฅ๐Ÿ’Ž MEGA NIEUWE STASH PREVIEW SIZE INIT! ๐Ÿ’Ž๐Ÿ”ฅ - let stashPreviewSizeRaw = UserDefaults.standard.string(forKey: SettingsKey.stashPreviewSize) ?? StashPreviewSize.medium.rawValue - self.stashPreviewSize = StashPreviewSize(rawValue: stashPreviewSizeRaw) ?? .medium - - // ๐Ÿ”ฅ๐Ÿ’ฅโšก HYPERMODE STASH GRID INIT! โšก๐Ÿ’ฅ๐Ÿ”ฅ - let stashGridModeRaw = UserDefaults.standard.string(forKey: SettingsKey.stashGridMode) ?? StashGridMode.fixedColumns.rawValue - self.stashGridMode = StashGridMode(rawValue: stashGridModeRaw) ?? .fixedColumns - - self.stashMaxColumns = UserDefaults.standard.object(forKey: SettingsKey.stashMaxColumns) as? Int ?? 2 - // HYPERMODE SAFETY: Clamp to 1-5 range! - self.stashMaxColumns = max(1, min(5, self.stashMaxColumns)) - - self.stashMaxRows = UserDefaults.standard.object(forKey: SettingsKey.stashMaxRows) as? Int ?? 1 - // HYPERMODE SAFETY: Clamp to 1-5 range! - self.stashMaxRows = max(1, min(5, self.stashMaxRows)) - - // ๐Ÿ”ฅ NIEUW: Persistent stash init - self.persistentStash = UserDefaults.standard.object(forKey: SettingsKey.persistentStash) as? Bool ?? false - - // ๐Ÿ”„ UPDATE SETTINGS INIT - self.automaticUpdates = UserDefaults.standard.object(forKey: SettingsKey.automaticUpdates) as? Bool ?? true - self.includePreReleases = UserDefaults.standard.object(forKey: SettingsKey.includePreReleases) as? Bool ?? false - - // ๐ŸŽน CUSTOM SHORTCUT SETTINGS INIT - self.useCustomShortcut = UserDefaults.standard.object(forKey: SettingsKey.useCustomShortcut) as? Bool ?? false - self.customShortcutModifiers = UserDefaults.standard.object(forKey: SettingsKey.customShortcutModifiers) as? UInt ?? 0 - self.customShortcutKey = UserDefaults.standard.object(forKey: SettingsKey.customShortcutKey) as? UInt16 ?? 0 - - // ๐Ÿ”Š SOUND SETTINGS INIT - self.screenshotSoundVolume = UserDefaults.standard.object(forKey: SettingsKey.screenshotSoundVolume) as? Float ?? 0.1 - - if let soundTypeData = UserDefaults.standard.data(forKey: SettingsKey.screenshotSoundType), - let soundType = try? JSONDecoder().decode(ScreenshotSoundType.self, from: soundTypeData) { - self.screenshotSoundType = soundType - } else { - self.screenshotSoundType = .pop - } - - // ๐Ÿ—‚๏ธ CACHE MANAGEMENT INIT - if let cacheRetentionData = UserDefaults.standard.data(forKey: SettingsKey.cacheRetentionTime), - let retentionTime = try? JSONDecoder().decode(CacheRetentionTime.self, from: cacheRetentionData) { - self.cacheRetentionTime = retentionTime - } else { - self.cacheRetentionTime = .oneWeek - } - - // Initialize thumbnailDisplayScreen - let _ = UserDefaults.standard.string(forKey: SettingsKey.thumbnailDisplayScreen) ?? ThumbnailDisplayScreen.automatic.rawValue - // No need to set it since it's a computed property - - // Load action order with migration for new actions - if let savedOrder = UserDefaults.standard.stringArray(forKey: SettingsManager.actionOrderKey) { - var loadedOrder = savedOrder.compactMap { ActionType(rawValue: $0) } - - // Migration: ensure all new actions are included - let allActions = ActionType.allCases - for action in allActions { - if !loadedOrder.contains(action) { - loadedOrder.append(action) - } - } - - actionOrder = loadedOrder - } else { - actionOrder = ActionType.allCases - } - - // Initialize hasCompletedFirstLaunch - self.hasCompletedFirstLaunch = UserDefaults.standard.object(forKey: SettingsKey.hasCompletedFirstLaunch) as? Bool ?? false - - isInitializing = false // RESET FLAG - } - - private enum CodingKeys: String, CodingKey { - case screenshotFolder - case filenamePrefix - case filenameFormatPreset - case filenameCustomFormat - case saveAfterEdit - case playSoundOnCapture - case thumbnailTimer - case closeAfterDrag - case thumbnailFixedSize - case showFolderButton - case startAppOnLogin - case autoSaveScreenshot - case closeAfterSave - } - - required init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - screenshotFolder = try container.decodeIfPresent(String.self, forKey: .screenshotFolder) - filenamePrefix = try container.decodeIfPresent(String.self, forKey: .filenamePrefix) ?? "Screenshot" - filenameCustomFormat = try container.decodeIfPresent(String.self, forKey: .filenameCustomFormat) ?? "{YYYY}-{MM}-{DD}_{hh}.{mm}.{ss}" - saveAfterEdit = try container.decodeIfPresent(Bool.self, forKey: .saveAfterEdit) ?? false - playSoundOnCapture = try container.decodeIfPresent(Bool.self, forKey: .playSoundOnCapture) ?? true - thumbnailTimer = try container.decodeIfPresent(Int.self, forKey: .thumbnailTimer) ?? 0 - closeAfterDrag = try container.decodeIfPresent(Bool.self, forKey: .closeAfterDrag) ?? false - let presetRaw = try container.decodeIfPresent(Int.self, forKey: .filenameFormatPreset) ?? FilenameFormatPreset.macOSStyle.rawValue - filenameFormatPreset = FilenameFormatPreset(rawValue: presetRaw) ?? .macOSStyle - thumbnailFixedSize = try container.decodeIfPresent(ThumbnailFixedSize.self, forKey: .thumbnailFixedSize) ?? .medium - showFolderButton = try container.decodeIfPresent(Bool.self, forKey: .showFolderButton) ?? true - startAppOnLogin = try container.decodeIfPresent(Bool.self, forKey: .startAppOnLogin) ?? false - autoSaveScreenshot = try container.decodeIfPresent(Bool.self, forKey: .autoSaveScreenshot) ?? false - closeAfterSave = try container.decodeIfPresent(Bool.self, forKey: .closeAfterSave) ?? false - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(screenshotFolder, forKey: .screenshotFolder) - try container.encode(filenamePrefix, forKey: .filenamePrefix) - try container.encode(filenameFormatPreset.rawValue, forKey: .filenameFormatPreset) - try container.encode(filenameCustomFormat, forKey: .filenameCustomFormat) - try container.encode(saveAfterEdit, forKey: .saveAfterEdit) - try container.encode(playSoundOnCapture, forKey: .playSoundOnCapture) - try container.encode(thumbnailTimer, forKey: .thumbnailTimer) - try container.encode(closeAfterDrag, forKey: .closeAfterDrag) - try container.encode(thumbnailFixedSize, forKey: .thumbnailFixedSize) - try container.encode(showFolderButton, forKey: .showFolderButton) - try container.encode(startAppOnLogin, forKey: .startAppOnLogin) - try container.encode(autoSaveScreenshot, forKey: .autoSaveScreenshot) - try container.encode(closeAfterSave, forKey: .closeAfterSave) - } - - func resetToDefaults() { - objectWillChange.send() // Stuur eenmalig aan het begin - - // Wijs defaults direct toe aan de properties; de didSets zorgen voor opslaan en notificaties. - screenshotFolder = defaultScreenshotFolder() - filenamePrefix = "Schermafbeelding" - filenameFormatPreset = .macOSStyle - filenameCustomFormat = "{YYYY}-{MM}-{DD}_{hh}.{mm}.{ss}" - saveAfterEdit = false - playSoundOnCapture = true - thumbnailTimer = 0 - closeAfterDrag = false - thumbnailFixedSize = .medium - showFolderButton = true - startAppOnLogin = false - autoSaveScreenshot = false - closeAfterSave = false - isRenameActionEnabled = true - isStashActionEnabled = true - isOCRActionEnabled = true - isClipboardActionEnabled = true - isCancelActionEnabled = true - isRemoveActionEnabled = true - isDeleteActionEnabled = true - isCancelDragActionEnabled = true - stashAlwaysOnTop = false - hideDesktopIconsDuringScreenshot = false - - // ๐Ÿ”ฅ๐Ÿ’Ž MEGA NIEUWE RESET VOOR STASH PREVIEW SIZE! ๐Ÿ’Ž๐Ÿ”ฅ - stashPreviewSize = .medium - - // ๐Ÿ”ฅ๐Ÿ’ฅโšก HYPERMODE STASH GRID RESET! โšก๐Ÿ’ฅ๐Ÿ”ฅ - stashGridMode = .fixedColumns - stashMaxColumns = 2 - stashMaxRows = 1 - - // ๐Ÿ”ฅ NIEUW: Reset persistent stash - persistentStash = false - - // ๐Ÿ”„ RESET UPDATE SETTINGS - automaticUpdates = true - includePreReleases = false - - // ๐ŸŽน RESET CUSTOM SHORTCUT SETTINGS - useCustomShortcut = false - customShortcutModifiers = 0 - customShortcutKey = 0 - - - // Verwijder de saveSettings() aanroep. - // Verwijder de individuele NotificationCenter.default.post calls hier; didSets handelen dat af. - } - - private func defaultScreenshotFolder() -> String? { - // Voorbeeld: probeer ~/Pictures/Screenshots, anders nil - if let picturesURL = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask).first { - let screenshotsURL = picturesURL.appendingPathComponent("Screenshots") - // Maak de map als hij niet bestaat (optioneel) - // try? FileManager.default.createDirectory(at: screenshotsURL, withIntermediateDirectories: true, attributes: nil) - return screenshotsURL.path - } - return nil - } - - func resetActionOrder() { - actionOrder = ActionType.allCases - } - - func moveAction(_ action: ActionType, direction: Int) { - guard let currentIndex = actionOrder.firstIndex(of: action) else { return } - let newIndex = currentIndex + direction - - guard newIndex >= 0 && newIndex < actionOrder.count else { return } - - actionOrder.swapAt(currentIndex, newIndex) - } - - // saveSettings() is waarschijnlijk niet nodig als @Published didSets goed werken. - // Als je het toch wilt: - /* - func saveSettings() { - UserDefaults.standard.set(screenshotFolder, forKey: SettingsKey.screenshotFolder) - // ... etc. voor alle settings ... - UserDefaults.standard.synchronize() // Optioneel, gebeurt periodiek - } - */ -} - -// MARK: - Settings Window UI (Nieuwe structuur met TabView) - -struct SettingsSnapshot { - var screenshotFolder: String? - var thumbnailTimer: Int - var closeAfterDrag: Bool - var thumbnailFixedSize: ThumbnailFixedSize - var showFolderButton: Bool - var closeAfterSave: Bool - var playSoundOnCapture: Bool - var filenamePrefix: String - var filenameFormatPreset: FilenameFormatPreset - var filenameCustomFormat: String - var startAppOnLogin: Bool - var autoSaveScreenshot: Bool - var thumbnailDisplayScreen: ThumbnailDisplayScreen - var stashAlwaysOnTop: Bool - var hideDesktopIconsDuringScreenshot: Bool - - static func captureCurrent() -> SettingsSnapshot { - let s = SettingsManager.shared - return SettingsSnapshot( - screenshotFolder: s.screenshotFolder, - thumbnailTimer: s.thumbnailTimer, - closeAfterDrag: s.closeAfterDrag, - thumbnailFixedSize: s.thumbnailFixedSize, - showFolderButton: s.showFolderButton, - closeAfterSave: s.closeAfterSave, - playSoundOnCapture: s.playSoundOnCapture, - filenamePrefix: s.filenamePrefix, - filenameFormatPreset: s.filenameFormatPreset, - filenameCustomFormat: s.filenameCustomFormat, - startAppOnLogin: s.startAppOnLogin, - autoSaveScreenshot: s.autoSaveScreenshot, - thumbnailDisplayScreen: s.thumbnailDisplayScreen, - stashAlwaysOnTop: s.stashAlwaysOnTop, - hideDesktopIconsDuringScreenshot: s.hideDesktopIconsDuringScreenshot - ) - } - - func apply(to manager: SettingsManager = SettingsManager.shared) { - manager.screenshotFolder = screenshotFolder - manager.thumbnailTimer = thumbnailTimer - manager.closeAfterDrag = closeAfterDrag - manager.thumbnailFixedSize = thumbnailFixedSize - manager.showFolderButton = showFolderButton - manager.closeAfterSave = closeAfterSave - manager.playSoundOnCapture = playSoundOnCapture - manager.filenamePrefix = filenamePrefix - manager.filenameFormatPreset = filenameFormatPreset - manager.filenameCustomFormat = filenameCustomFormat - manager.startAppOnLogin = startAppOnLogin - manager.autoSaveScreenshot = autoSaveScreenshot - manager.thumbnailDisplayScreen = thumbnailDisplayScreen - manager.stashAlwaysOnTop = stashAlwaysOnTop - manager.hideDesktopIconsDuringScreenshot = hideDesktopIconsDuringScreenshot - } -} - -// MARK: - Cache Manager -struct CacheManager { - static let shared = CacheManager() - - private init() {} - - // Get thumbnail directory path - private var thumbnailDirectory: URL { - let appSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! - let shotScreenDirectory = appSupportDirectory.appendingPathComponent("ShotScreen") - let thumbnailsDirectory = shotScreenDirectory.appendingPathComponent("Thumbnails") - return thumbnailsDirectory - } - - // Calculate cache size in MB - func getCacheSize() -> Double { - do { - let contents = try FileManager.default.contentsOfDirectory(at: thumbnailDirectory, includingPropertiesForKeys: [.fileSizeKey], options: []) - - var totalSize: Int64 = 0 - for fileURL in contents { - // Skip thumbnail restoration directory from cache size calculation - if fileURL.lastPathComponent == "thumbnail_restoration" { - continue - } - - do { - let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey]) - if let fileSize = resourceValues.fileSize { - totalSize += Int64(fileSize) - } - } catch { - print("โš ๏ธ Error getting file size for \(fileURL.lastPathComponent): \(error)") - } - } - - // Convert bytes to MB - return Double(totalSize) / (1024 * 1024) - } catch { - print("โš ๏ธ Error calculating cache size: \(error)") - return 0.0 - } - } - - // Get cache file count - func getCacheFileCount() -> Int { - do { - let contents = try FileManager.default.contentsOfDirectory(at: thumbnailDirectory, includingPropertiesForKeys: [.isDirectoryKey], options: []) - - var fileCount = 0 - for fileURL in contents { - // Skip thumbnail restoration directory - if fileURL.lastPathComponent == "thumbnail_restoration" { - continue - } - - // Only count PNG files, not directories - let resourceValues = try? fileURL.resourceValues(forKeys: [.isDirectoryKey]) - let isDirectory = resourceValues?.isDirectory ?? false - - if !isDirectory && fileURL.pathExtension == "png" { - fileCount += 1 - } - } - - return fileCount - } catch { - print("โš ๏ธ Error getting cache file count: \(error)") - return 0 - } - } - - // Clear all cache (except active thumbnails) - func clearCache(preserveActiveThumbnails: Bool = true) -> (deletedFiles: Int, savedSpace: Double) { - do { - let contents = try FileManager.default.contentsOfDirectory(at: thumbnailDirectory, includingPropertiesForKeys: [.fileSizeKey, .creationDateKey], options: []) - - var deletedFiles = 0 - var savedSpace: Int64 = 0 - let activeThumbnailPath = getActiveThumbnailPath() - - for fileURL in contents { - // Skip if this is the active thumbnail and we want to preserve it - if preserveActiveThumbnails && fileURL.path == activeThumbnailPath { - print("๐Ÿ”’ Preserving active thumbnail: \(fileURL.lastPathComponent)") - continue - } - - // Skip thumbnail restoration directory completely - if fileURL.lastPathComponent == "thumbnail_restoration" { - print("๐Ÿ”’ Preserving thumbnail restoration directory: \(fileURL.lastPathComponent)") - continue - } - - // Skip thumbnail restoration backup files - if fileURL.lastPathComponent.contains("latest_backup") { - print("๐Ÿ”’ Preserving thumbnail restoration backup file: \(fileURL.lastPathComponent)") - continue - } - - do { - let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey]) - let fileSize = resourceValues.fileSize ?? 0 - - try FileManager.default.removeItem(at: fileURL) - deletedFiles += 1 - savedSpace += Int64(fileSize) - print("๐Ÿ—‘๏ธ Deleted cache file: \(fileURL.lastPathComponent)") - } catch { - print("โš ๏ธ Failed to delete \(fileURL.lastPathComponent): \(error)") - } - } - - let savedSpaceMB = Double(savedSpace) / (1024 * 1024) - print("โœ… Cache cleanup complete: \(deletedFiles) files deleted, \(String(format: "%.1f", savedSpaceMB)) MB freed") - return (deletedFiles, savedSpaceMB) - - } catch { - print("โŒ Error during cache cleanup: \(error)") - return (0, 0.0) - } - } - - // Clean cache based on retention time - func cleanupOldCache() { - let retentionTime = SettingsManager.shared.cacheRetentionTime - - // Don't cleanup if retention is set to forever - guard let maxAge = retentionTime.timeInterval else { - print("๐Ÿ—‚๏ธ Cache retention set to forever - no automatic cleanup") - return - } - - do { - let contents = try FileManager.default.contentsOfDirectory(at: thumbnailDirectory, includingPropertiesForKeys: [.creationDateKey, .fileSizeKey], options: []) - let cutoffDate = Date().addingTimeInterval(-maxAge) - let activeThumbnailPath = getActiveThumbnailPath() - - print("๐Ÿงช CLEANUP: Checking \(contents.count) files. Cutoff time: \(cutoffDate). Max age: \(Int(maxAge))s") - - var deletedFiles = 0 - var savedSpace: Int64 = 0 - var checkedFiles = 0 - - for fileURL in contents { - // Skip active thumbnail - if fileURL.path == activeThumbnailPath { - print("๐Ÿงช CLEANUP: Skipping active thumbnail: \(fileURL.lastPathComponent)") - continue - } - - // Skip thumbnail restoration directory completely - if fileURL.lastPathComponent == "thumbnail_restoration" { - continue - } - - // Skip thumbnail restoration backup files - if fileURL.lastPathComponent.contains("latest_backup") { - continue - } - - do { - let resourceValues = try fileURL.resourceValues(forKeys: [.creationDateKey, .fileSizeKey]) - - if let creationDate = resourceValues.creationDate { - checkedFiles += 1 - let age = Date().timeIntervalSince(creationDate) - print("๐Ÿงช CLEANUP: \(fileURL.lastPathComponent) - age: \(Int(age))s, created: \(creationDate)") - - if creationDate < cutoffDate { - let fileSize = resourceValues.fileSize ?? 0 - try FileManager.default.removeItem(at: fileURL) - deletedFiles += 1 - savedSpace += Int64(fileSize) - print("๐Ÿ—‘๏ธ DELETED: \(fileURL.lastPathComponent) (was \(Int(age))s old)") - } else { - print("โœ… KEEPING: \(fileURL.lastPathComponent) (only \(Int(age))s old)") - } - } - } catch { - print("โš ๏ธ Error processing \(fileURL.lastPathComponent): \(error)") - } - } - - if deletedFiles > 0 { - let savedSpaceMB = Double(savedSpace) / (1024 * 1024) - print("โœ… Auto cache cleanup: \(deletedFiles) old files deleted, \(String(format: "%.1f", savedSpaceMB)) MB freed") - } else if checkedFiles > 0 { - print("๐Ÿงช CLEANUP: No files old enough to delete (\(checkedFiles) files checked)") - } else { - print("๐Ÿงช CLEANUP: No cache files found to check") - } - - } catch { - print("โŒ Error during automatic cache cleanup: \(error)") - } - } - - // Get active thumbnail path (to avoid deleting currently open thumbnail) - private func getActiveThumbnailPath() -> String? { - // Try to get the active thumbnail URL from ScreenshotApp - if let app = NSApp.delegate as? ScreenshotApp, - let tempURL = app.getTempURL() { - return tempURL.path - } - return nil - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/SettingsModels.swift b/ShotScreen/Sources/SettingsModels.swift deleted file mode 100644 index 325de93..0000000 --- a/ShotScreen/Sources/SettingsModels.swift +++ /dev/null @@ -1,413 +0,0 @@ -import AppKit -import SwiftUI // Needed for CGSize, CaseIterable, Identifiable, Codable - -// Define Notification Names -extension Notification.Name { - static let closeAfterDragSettingChanged = Notification.Name("closeAfterDragSettingChanged") - static let closeAfterSaveSettingChanged = Notification.Name("closeAfterSaveSettingChanged") - static let playSoundOnCaptureSettingChanged = Notification.Name("playSoundOnCaptureSettingChanged") - static let startAppOnLoginSettingChanged = Notification.Name("startAppOnLoginSettingChanged") - static let thumbnailSizeSettingChanged = Notification.Name("thumbnailSizeSettingChanged") - static let screenshotFolderChanged = Notification.Name("screenshotFolderChanged") - static let filenameSettingsChanged = Notification.Name("filenameSettingsChanged") - static let saveAfterEditSettingChanged = Notification.Name("saveAfterEditSettingChanged") - static let thumbnailTimerSettingChanged = Notification.Name("thumbnailTimerSettingChanged") - static let thumbnailPositionSettingChanged = Notification.Name("thumbnailPositionSettingChanged") - static let showFolderButtonSettingChanged = Notification.Name("showFolderButtonSettingChanged") - static let autoSaveScreenshotSettingChanged = Notification.Name("autoSaveScreenshotSettingChanged") - static let stashAlwaysOnTopSettingChanged = Notification.Name("stashAlwaysOnTopSettingChanged") - static let shortcutSettingChanged = Notification.Name("shortcutSettingChanged") - static let windowCaptureSettingChanged = Notification.Name("windowCaptureSettingChanged") - - static let hideDesktopIconsSettingChanged = Notification.Name("hideDesktopIconsSettingChanged") - static let hideDesktopWidgetsSettingChanged = Notification.Name("hideDesktopWidgetsSettingChanged") - static let stashPreviewSizeChanged = Notification.Name("stashPreviewSizeChanged") - // ๐Ÿ”ฅ๐Ÿ’ฅ HYPERMODE GRID NOTIFICATIONS! ๐Ÿ’ฅ๐Ÿ”ฅ - static let stashGridModeChanged = Notification.Name("stashGridModeChanged") - static let stashGridConfigChanged = Notification.Name("stashGridConfigChanged") - // ๐Ÿ”ฅ NIEUW: Persistent stash notification - static let persistentStashChanged = Notification.Name("persistentStashChanged") - // Add more here if needed for other live updates -} - -// NEW: Enum for Screenshot Save Mode -enum ScreenshotSaveMode: Int, CaseIterable, Identifiable { - case automatic = 0 - case manual = 1 - - var id: Int { self.rawValue } - - var description: String { - switch self { - case .automatic: return "Always save screenshot to folder" - case .manual: return "Manually decide after screenshot" - } - } -} - -// Settings keys -enum SettingsKey { - static let screenshotFolder = "screenshotFolder" - static let thumbnailTimer = "thumbnailTimer" - static let closeAfterDrag = "closeAfterDrag" - static let thumbnailPosition = "thumbnailPosition" - static let showFolderButton = "showFolderButton" - static let closeAfterSave = "closeAfterSave" - static let playSoundOnCapture = "playSoundOnCapture" - static let filenamePrefix = "filenamePrefix" - static let filenameFormatPreset = "filenameFormatPreset" - static let filenameCustomFormat = "filenameCustomFormat" - static let stashWindowBorderWidth = "stashWindowBorderWidth" - static let isRenameActionEnabled = "isRenameActionEnabled" - static let isStashActionEnabled = "isStashActionEnabled" - static let isOCRActionEnabled = "isOCRActionEnabled" - static let isClipboardActionEnabled = "isClipboardActionEnabled" - static let isBackgroundRemoveActionEnabled = "isBackgroundRemoveActionEnabled" - static let isCancelActionEnabled = "isCancelActionEnabled" - static let isRemoveActionEnabled = "isRemoveActionEnabled" - static let isAction3Enabled = "isAction3Enabled" - static let isAction4Enabled = "isAction4Enabled" - static let isDeleteActionEnabled = "isDeleteActionEnabled" - static let isCancelDragActionEnabled = "isCancelDragActionEnabled" - static let startAppOnLogin = "startAppOnLogin" - static let autoSaveScreenshot = "autoSaveScreenshot" - static let saveAfterEdit = "saveAfterEdit" - static let thumbnailDisplayScreen = "thumbnailDisplayScreen" - static let stashAlwaysOnTop = "stashAlwaysOnTop" - static let openOnStartup = "openOnStartup" - static let hasCompletedFirstLaunch = "hasCompletedFirstLaunch" - static let customShortcutModifiers = "customShortcutModifiers" - static let customShortcutKey = "customShortcutKey" - static let useCustomShortcut = "useCustomShortcut" - static let windowCaptureIncludeChildWindows = "windowCaptureIncludeChildWindows" - static let windowCaptureIncludeCursor = "windowCaptureIncludeCursor" - static let windowCaptureHighResolution = "windowCaptureHighResolution" - static let windowCaptureShowSelectionUI = "windowCaptureShowSelectionUI" - - static let hideDesktopIconsDuringScreenshot = "hideDesktopIconsDuringScreenshot" - static let hideDesktopWidgetsDuringScreenshot = "hideDesktopWidgetsDuringScreenshot" - static let stashPreviewSize = "stashPreviewSize" - - // ๐Ÿ”ฅ๐Ÿ’ฅ HYPERMODE STASH GRID KEYS! ๐Ÿ’ฅ๐Ÿ”ฅ - static let stashGridMode = "stashGridMode" - static let stashMaxColumns = "stashMaxColumns" - static let stashMaxRows = "stashMaxRows" - - // ๐Ÿ”ฅ NIEUW: Persistent stash setting - static let persistentStash = "persistentStash" - - // ๐Ÿ”„ UPDATE SETTINGS - static let automaticUpdates = "automaticUpdates" - static let includePreReleases = "includePreReleases" - - // ๐Ÿ”Š SOUND SETTINGS - static let screenshotSoundVolume = "screenshotSoundVolume" - static let screenshotSoundType = "screenshotSoundType" - - // ๐Ÿ—‚๏ธ CACHE MANAGEMENT SETTINGS - static let cacheRetentionTime = "cacheRetentionTime" - - // ๐ŸŽจ BACKGROUND REMOVAL METHOD PREFERENCE - static let preferredBackgroundRemovalMethod = "preferredBackgroundRemovalMethod" - -} - -enum ThumbnailPosition: Int, CaseIterable, Codable { - case rightBottom = 0 - - var description: String { - switch self { - case .rightBottom: return "Bottom Right" - } - } -} - -enum ThumbnailDisplayScreen: String, CaseIterable, Codable { - case automatic = "automatic" - case screen1 = "screen1" - case screen2 = "screen2" - case screen3 = "screen3" - case screen4 = "screen4" - case screen5 = "screen5" - - var description: String { - switch self { - case .automatic: return "Automatic (where mouse is)" - case .screen1: return "Screen 1" - case .screen2: return "Screen 2" - case .screen3: return "Screen 3" - case .screen4: return "Screen 4" - case .screen5: return "Screen 5" - } - } - - func getDisplayName(for screenIndex: Int, screenName: String?) -> String { - switch self { - case .automatic: return "Automatic (where mouse is)" - case .screen1, .screen2, .screen3, .screen4, .screen5: - let screenNumber = screenIndex + 1 - if let name = screenName, !name.isEmpty { - return "Screen \(screenNumber) (\(name))" - } else { - return "Screen \(screenNumber)" - } - } - } -} - -// NEW: Enum for filename format presets -enum FilenameFormatPreset: Int, CaseIterable, Codable { - case macOSStyle = 0 // Screenshot {YYYY}-{MM}-{DD} at {hh}.{mm}.{ss} - case compactDateTime = 1 // {YYYY}-{MM}-{DD}_{hh}-{mm}-{ss} - case superCompactDateTime = 2 // {YYYYMMDD}_{hhmmss} - case timestamp = 3 // Unix timestamp - case prefixOnly = 4 // Prefix only - case custom = 5 // Custom format - - var description: String { - switch self { - case .macOSStyle: return "macOS Style (Screenshot YYYY-MM-DD at hh.mm.ss)" - case .compactDateTime: return "Compact (YYYY-MM-DD_hh-mm-ss)" - case .superCompactDateTime: return "Super Compact (YYYYMMDD_hhmmss)" - case .timestamp: return "Unix Timestamp" - case .prefixOnly: return "Prefix Only" - case .custom: return "Custom..." - } - } - - // Placeholder voor custom format string (alleen relevant voor .custom) - // De feitelijke custom string wordt apart opgeslagen. -} - -// VOEG DEZE ENUM TOE (ergens bovenaan, of logisch gegroepeerd) -enum ThumbnailFixedSize: String, CaseIterable, Codable { - case small, medium, large, xLarge, xxLarge, xxxLarge - - var dimensions: CGSize { - switch self { - case .small: return CGSize(width: 120, height: 90) - case .medium: return CGSize(width: 180, height: 135) - case .large: return CGSize(width: 240, height: 180) - case .xLarge: return CGSize(width: 300, height: 225) - case .xxLarge: return CGSize(width: 360, height: 270) - case .xxxLarge: return CGSize(width: 420, height: 315) - } - } - - var displayName: String { // Voor de Picker - switch self { - case .small: return "Small" - case .medium: return "Medium" - case .large: return "Large" - case .xLarge: return "X-Large" - case .xxLarge: return "XX-Large" - case .xxxLarge: return "XXX-Large" - } - } -} - -// ๐Ÿ”ฅ๐Ÿ’Ž MEGA NIEUWE STASH PREVIEW SIZE ENUM! ๐Ÿ’Ž๐Ÿ”ฅ -enum StashPreviewSize: String, CaseIterable, Codable { - case xSmall = "xSmall" // 25% van origineel - case small = "small" // 40% van origineel - case medium = "medium" // 60% van origineel - case large = "large" // 80% van origineel - case xLarge = "xLarge" // 100% van origineel (full size!) - - var percentage: CGFloat { - switch self { - case .xSmall: return 0.25 - case .small: return 0.40 - case .medium: return 0.60 - case .large: return 0.80 - case .xLarge: return 1.00 - } - } - - var displayName: String { - switch self { - case .xSmall: return "Extra Small" - case .small: return "Small" - case .medium: return "Medium" - case .large: return "Large" - case .xLarge: return "Extra Large" - } - } -} - -// ๐Ÿ”ฅ๐Ÿ’ฅโšก HYPERMODE STASH GRID MODE ENUM! โšก๐Ÿ’ฅ๐Ÿ”ฅ -enum StashGridMode: String, CaseIterable, Codable, Identifiable { - case fixedColumns = "fixedColumns" // Max kolommen, auto rijen (huidige systeem) - case fixedRows = "fixedRows" // Max rijen, auto kolommen (horizontale strip!) - - var id: String { rawValue } - - var displayName: String { - switch self { - case .fixedColumns: return "Fixed Columns, Auto Rows" - case .fixedRows: return "Fixed Rows, Auto Columns" - } - } - - var description: String { - switch self { - case .fixedColumns: return "Grows vertically with more rows" - case .fixedRows: return "Grows horizontally with more columns" - } - } - - var iconName: String { - switch self { - case .fixedColumns: return "rectangle.split.1x3" // Verticale layout (1 kolom, 3 rijen = verticaal groeien) - case .fixedRows: return "rectangle.split.3x1" // Horizontale layout (3 kolommen, 1 rij = horizontaal groeien) - } - } -} - -// ๐Ÿ”Š SCREENSHOT SOUND TYPE ENUM -enum ScreenshotSoundType: String, CaseIterable, Codable, Identifiable { - case pop = "Pop" - case glass = "Glass" - case ping = "Ping" - case purr = "Purr" - case tink = "Tink" - case basso = "Basso" - case blow = "Blow" - case bottle = "Bottle" - case frog = "Frog" - case funk = "Funk" - case hero = "Hero" - case morse = "Morse" - case sosumi = "Sosumi" - case submarine = "Submarine" - - var id: String { rawValue } - - var displayName: String { - switch self { - case .pop: return "Pop" - case .glass: return "Glass" - case .ping: return "Ping" - case .purr: return "Purr" - case .tink: return "Tink" - case .basso: return "Basso" - case .blow: return "Blow" - case .bottle: return "Bottle" - case .frog: return "Frog" - case .funk: return "Funk" - case .hero: return "Hero" - case .morse: return "Morse" - case .sosumi: return "Sosumi" - case .submarine: return "Submarine" - } - } - - var systemSoundName: String { - return rawValue - } -} - -// ๐Ÿ—‚๏ธ CACHE RETENTION TIME ENUM -enum CacheRetentionTime: String, CaseIterable, Codable, Identifiable { - case oneHour = "1h" - case sixHours = "6h" - case twelveHours = "12h" - case oneDay = "1d" - case threeDays = "3d" - case oneWeek = "1w" - case twoWeeks = "2w" - case oneMonth = "1m" - case forever = "forever" - - var id: String { rawValue } - - var displayName: String { - switch self { - case .oneHour: return "1 Hour" - case .sixHours: return "6 Hours" - case .twelveHours: return "12 Hours" - case .oneDay: return "1 Day" - case .threeDays: return "3 Days" - case .oneWeek: return "1 Week" - case .twoWeeks: return "2 Weeks" - case .oneMonth: return "1 Month" - case .forever: return "Forever (Manual delete)" - } - } - - var timeInterval: TimeInterval? { - switch self { - case .oneHour: return 3600 - case .sixHours: return 3600 * 6 - case .twelveHours: return 3600 * 12 - case .oneDay: return 86400 - case .threeDays: return 86400 * 3 - case .oneWeek: return 86400 * 7 - case .twoWeeks: return 86400 * 14 - case .oneMonth: return 86400 * 30 - case .forever: return nil // Never delete - } - } -} - -// Add after the existing SettingsKey enum -enum ActionType: String, CaseIterable, Identifiable { - case rename = "Rename" - case stash = "Stash" - case ocr = "OCR" - case clipboard = "Clipboard" - case backgroundRemove = "BackgroundRemove" - case cancel = "Cancel" - case remove = "Remove" - - var id: String { self.rawValue } - - var settingsKey: String { - switch self { - case .rename: return SettingsKey.isRenameActionEnabled - case .stash: return SettingsKey.isStashActionEnabled - case .ocr: return SettingsKey.isOCRActionEnabled - case .clipboard: return SettingsKey.isClipboardActionEnabled - case .backgroundRemove: return SettingsKey.isBackgroundRemoveActionEnabled - case .cancel: return SettingsKey.isCancelActionEnabled - case .remove: return SettingsKey.isRemoveActionEnabled - } - } - - var displayName: String { - switch self { - case .rename: return "Rename" - case .stash: return "Stash" - case .ocr: return "Text Extract" - case .clipboard: return "Clipboard" - case .backgroundRemove: return "Remove Background" - case .cancel: return "Cancel" - case .remove: return "Remove" - } - } -} - -// MARK: - Background Removal Method Preference -enum BackgroundRemovalMethod: String, CaseIterable, Identifiable { - case auto = "auto" - case rmbg = "rmbg" - case vision = "vision" - - var id: String { self.rawValue } - - var displayName: String { - switch self { - case .auto: return "Auto (RMBG โ†’ Vision fallback)" - case .rmbg: return "RMBG-1.4 only" - case .vision: return "Vision Framework only" - } - } - - var description: String { - switch self { - case .auto: return "Tries RMBG-1.4 first, falls back to Vision if unavailable" - case .rmbg: return "Uses only RMBG-1.4 model (requires download)" - case .vision: return "Uses only Apple's Vision Framework (always available)" - } - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/SettingsUI.swift b/ShotScreen/Sources/SettingsUI.swift deleted file mode 100644 index a850a8a..0000000 --- a/ShotScreen/Sources/SettingsUI.swift +++ /dev/null @@ -1,2868 +0,0 @@ -import AppKit -import SwiftUI // Needed for TabView and materials - -// MARK: - Settings Window UI (Nieuwe structuur met TabView) - -struct SettingsTabView: View { - @State private var selectedTab: Int = 0 - @ObservedObject private var settings = SettingsManager.shared - - // State voor Folder Path (omdat het via een panel wordt gezet) - @State private var folderPathDisplay: String - - // Temporary state voor alle settings - only applied on Apply - @State private var tempThumbnailTimer: Double - @State private var tempFilenamePrefix: String - @State private var tempFilenameFormatPreset: FilenameFormatPreset - @State private var tempFilenameCustomFormat: String - @State private var tempThumbnailFixedSize: ThumbnailFixedSize - @State private var tempShowFolderButton: Bool - @State private var tempCloseAfterDrag: Bool - @State private var tempCloseAfterSave: Bool - @State private var tempPlaySoundOnCapture: Bool - - // ๐Ÿ”Š NEW: Sound settings temporary state - @State private var tempScreenshotSoundVolume: Float - @State private var tempScreenshotSoundType: ScreenshotSoundType - - // ๐ŸŽจ NEW: BGR method preference temporary state - @State private var tempPreferredBackgroundRemovalMethod: BackgroundRemovalMethod - - // ๐Ÿ—‚๏ธ NEW: Cache management temporary state - @State private var tempCacheRetentionTime: CacheRetentionTime - @State private var cacheSize: Double = 0.0 - @State private var cacheFileCount: Int = 0 - @State private var showClearCacheConfirmation = false - @State private var tempAutoSaveScreenshot: Bool - @State private var tempActionOrder: [ActionType] - @State private var tempIsRenameActionEnabled: Bool - @State private var tempIsStashActionEnabled: Bool - @State private var tempIsOCRActionEnabled: Bool - @State private var tempIsClipboardActionEnabled: Bool - @State private var tempIsBackgroundRemoveActionEnabled: Bool - @State private var tempIsCancelActionEnabled: Bool - @State private var tempIsRemoveActionEnabled: Bool - @State private var tempScreenshotFolder: String? - @State private var tempThumbnailDisplayScreen: ThumbnailDisplayScreen - @State private var tempStashAlwaysOnTop: Bool - @State private var tempPersistentStash: Bool - - // ๐Ÿ”„ UPDATE SETTINGS TEMP VARS - @State private var tempHideDesktopIconsDuringScreenshot: Bool - @State private var tempHideDesktopWidgetsDuringScreenshot: Bool - @State private var tempStashPreviewSize: StashPreviewSize - - // ๐Ÿ”ฅ๐Ÿ’ฅโšก HYPERMODE STASH GRID TEMP VARIABLES! โšก๐Ÿ’ฅ๐Ÿ”ฅ - @State private var tempStashGridMode: StashGridMode - @State private var tempStashMaxColumns: Int - @State private var tempStashMaxRows: Int - - // NEW: Temporary state for custom modifier settings - - - // NEW: animatie-state voor de Apply-knop - @State private var showApplyConfirmation = false - @State private var showUnsavedChangesAlert = false - @State private var forceClose = false // Flag to bypass unsaved changes check - @State private var showLoginInstructionsPopup = false // NEW: For login instructions popup - - // AI Models state - @State private var isDownloadingModel = false - @State private var downloadProgress: Double = 0.0 - @State private var showDownloadConfirmation = false - @State private var showRemoveConfirmation = false - @State private var isRMBGModelInstalled = false - - private let originalSettings: SettingsSnapshot - private let sampleThumbnailImage = NSImage(contentsOf: URL(fileURLWithPath: "/Volumes/External HD/Users/nick/Desktop/ss 1.0/ScreenShot/image/ChatGPT Image May 9, 2025, 11_51_47 AM.png")) - - init() { - _folderPathDisplay = State(initialValue: SettingsManager.shared.screenshotFolder ?? "Not selected") - - let timerVal = SettingsManager.shared.thumbnailTimer - _tempThumbnailTimer = State(initialValue: (timerVal == 0) ? 31.0 : max(5.0, Double(timerVal))) - _tempFilenamePrefix = State(initialValue: SettingsManager.shared.filenamePrefix) - _tempFilenameFormatPreset = State(initialValue: SettingsManager.shared.filenameFormatPreset) - _tempFilenameCustomFormat = State(initialValue: SettingsManager.shared.filenameCustomFormat) - _tempThumbnailFixedSize = State(initialValue: SettingsManager.shared.thumbnailFixedSize) - _tempShowFolderButton = State(initialValue: SettingsManager.shared.showFolderButton) - _tempCloseAfterDrag = State(initialValue: SettingsManager.shared.closeAfterDrag) - _tempCloseAfterSave = State(initialValue: SettingsManager.shared.closeAfterSave) - _tempPlaySoundOnCapture = State(initialValue: SettingsManager.shared.playSoundOnCapture) - _tempScreenshotSoundVolume = State(initialValue: SettingsManager.shared.screenshotSoundVolume) - _tempScreenshotSoundType = State(initialValue: SettingsManager.shared.screenshotSoundType) - _tempPreferredBackgroundRemovalMethod = State(initialValue: SettingsManager.shared.preferredBackgroundRemovalMethod) - _tempCacheRetentionTime = State(initialValue: SettingsManager.shared.cacheRetentionTime) - _tempAutoSaveScreenshot = State(initialValue: SettingsManager.shared.autoSaveScreenshot) - _tempActionOrder = State(initialValue: SettingsManager.shared.actionOrder) - _tempIsRenameActionEnabled = State(initialValue: SettingsManager.shared.isRenameActionEnabled) - _tempIsStashActionEnabled = State(initialValue: SettingsManager.shared.isStashActionEnabled) - _tempIsOCRActionEnabled = State(initialValue: SettingsManager.shared.isOCRActionEnabled) - _tempIsClipboardActionEnabled = State(initialValue: SettingsManager.shared.isClipboardActionEnabled) - _tempIsBackgroundRemoveActionEnabled = State(initialValue: SettingsManager.shared.isBackgroundRemoveActionEnabled) - _tempIsCancelActionEnabled = State(initialValue: SettingsManager.shared.isCancelActionEnabled) - _tempIsRemoveActionEnabled = State(initialValue: SettingsManager.shared.isRemoveActionEnabled) - _tempScreenshotFolder = State(initialValue: SettingsManager.shared.screenshotFolder) - _tempThumbnailDisplayScreen = State(initialValue: SettingsManager.shared.thumbnailDisplayScreen) - _tempStashAlwaysOnTop = State(initialValue: SettingsManager.shared.stashAlwaysOnTop) - _tempPersistentStash = State(initialValue: SettingsManager.shared.persistentStash) - - // ๐Ÿ”„ UPDATE SETTINGS INIT - _tempHideDesktopIconsDuringScreenshot = State(initialValue: SettingsManager.shared.hideDesktopIconsDuringScreenshot) - _tempHideDesktopWidgetsDuringScreenshot = State(initialValue: SettingsManager.shared.hideDesktopWidgetsDuringScreenshot) - _tempStashPreviewSize = State(initialValue: SettingsManager.shared.stashPreviewSize) - - // ๐Ÿ”ฅ๐Ÿ’ฅโšก HYPERMODE STASH GRID INIT! โšก๐Ÿ’ฅ๐Ÿ”ฅ - _tempStashGridMode = State(initialValue: SettingsManager.shared.stashGridMode) - _tempStashMaxColumns = State(initialValue: SettingsManager.shared.stashMaxColumns) - _tempStashMaxRows = State(initialValue: SettingsManager.shared.stashMaxRows) - - // Initialize RMBG model status - _isRMBGModelInstalled = State(initialValue: Self.isModelInstalled()) - - // Initialize temporary custom modifier settings - - - self.originalSettings = SettingsSnapshot.captureCurrent() - } - - private var hasUnsavedChanges: Bool { - let currentSettings = SettingsManager.shared - return tempThumbnailTimer != ((currentSettings.thumbnailTimer == 0) ? 31.0 : max(5.0, Double(currentSettings.thumbnailTimer))) || - tempFilenamePrefix != currentSettings.filenamePrefix || - tempFilenameFormatPreset != currentSettings.filenameFormatPreset || - tempFilenameCustomFormat != currentSettings.filenameCustomFormat || - tempThumbnailFixedSize != currentSettings.thumbnailFixedSize || - tempShowFolderButton != currentSettings.showFolderButton || - tempCloseAfterDrag != currentSettings.closeAfterDrag || - tempCloseAfterSave != currentSettings.closeAfterSave || - tempPlaySoundOnCapture != currentSettings.playSoundOnCapture || - tempScreenshotSoundVolume != currentSettings.screenshotSoundVolume || - tempScreenshotSoundType != currentSettings.screenshotSoundType || - tempPreferredBackgroundRemovalMethod != currentSettings.preferredBackgroundRemovalMethod || - tempCacheRetentionTime != currentSettings.cacheRetentionTime || - tempAutoSaveScreenshot != currentSettings.autoSaveScreenshot || - tempActionOrder != currentSettings.actionOrder || - tempIsRenameActionEnabled != currentSettings.isRenameActionEnabled || - tempIsStashActionEnabled != currentSettings.isStashActionEnabled || - tempIsOCRActionEnabled != currentSettings.isOCRActionEnabled || - tempIsClipboardActionEnabled != currentSettings.isClipboardActionEnabled || - tempIsBackgroundRemoveActionEnabled != currentSettings.isBackgroundRemoveActionEnabled || - tempIsCancelActionEnabled != currentSettings.isCancelActionEnabled || - tempIsRemoveActionEnabled != currentSettings.isRemoveActionEnabled || - tempScreenshotFolder != currentSettings.screenshotFolder || - tempThumbnailDisplayScreen != currentSettings.thumbnailDisplayScreen || - tempStashAlwaysOnTop != currentSettings.stashAlwaysOnTop || - tempPersistentStash != currentSettings.persistentStash || - tempHideDesktopIconsDuringScreenshot != currentSettings.hideDesktopIconsDuringScreenshot || - tempHideDesktopWidgetsDuringScreenshot != currentSettings.hideDesktopWidgetsDuringScreenshot || - tempStashPreviewSize != currentSettings.stashPreviewSize || - // ๐Ÿ”ฅ๐Ÿ’ฅโšก HYPERMODE GRID CHANGE DETECTION! โšก๐Ÿ’ฅ๐Ÿ”ฅ - tempStashGridMode != currentSettings.stashGridMode || - tempStashMaxColumns != currentSettings.stashMaxColumns || - tempStashMaxRows != currentSettings.stashMaxRows - } - - private func moveActionInTemp(_ action: ActionType, direction: Int) { - guard let currentIndex = tempActionOrder.firstIndex(of: action) else { return } - let newIndex = currentIndex + direction - - guard newIndex >= 0 && newIndex < tempActionOrder.count else { return } - - tempActionOrder.swapAt(currentIndex, newIndex) - } - - private func applyAllChanges() { - let settings = SettingsManager.shared - - // Apply all temporary changes to actual settings - settings.screenshotFolder = tempScreenshotFolder - settings.filenamePrefix = tempFilenamePrefix - settings.filenameFormatPreset = tempFilenameFormatPreset - settings.filenameCustomFormat = tempFilenameCustomFormat - settings.thumbnailTimer = Int(tempThumbnailTimer.rounded()) >= 31 ? 0 : Int(tempThumbnailTimer.rounded()) - settings.thumbnailFixedSize = tempThumbnailFixedSize - settings.showFolderButton = tempShowFolderButton - settings.closeAfterDrag = tempCloseAfterDrag - settings.closeAfterSave = tempCloseAfterSave - settings.playSoundOnCapture = tempPlaySoundOnCapture - settings.screenshotSoundVolume = tempScreenshotSoundVolume - settings.screenshotSoundType = tempScreenshotSoundType - settings.preferredBackgroundRemovalMethod = tempPreferredBackgroundRemovalMethod - settings.cacheRetentionTime = tempCacheRetentionTime - settings.autoSaveScreenshot = tempAutoSaveScreenshot - settings.actionOrder = tempActionOrder - settings.isRenameActionEnabled = tempIsRenameActionEnabled - settings.isStashActionEnabled = tempIsStashActionEnabled - settings.isOCRActionEnabled = tempIsOCRActionEnabled - settings.isClipboardActionEnabled = tempIsClipboardActionEnabled - settings.isBackgroundRemoveActionEnabled = tempIsBackgroundRemoveActionEnabled - settings.isCancelActionEnabled = tempIsCancelActionEnabled - settings.isRemoveActionEnabled = tempIsRemoveActionEnabled - settings.thumbnailDisplayScreen = tempThumbnailDisplayScreen - settings.stashAlwaysOnTop = tempStashAlwaysOnTop - settings.persistentStash = tempPersistentStash - settings.hideDesktopIconsDuringScreenshot = tempHideDesktopIconsDuringScreenshot - settings.hideDesktopWidgetsDuringScreenshot = tempHideDesktopWidgetsDuringScreenshot - - // ๐Ÿ”„ UPDATE SETTINGS - settings.stashPreviewSize = tempStashPreviewSize - // ๐Ÿ”ฅ๐Ÿ’ฅโšก HYPERMODE GRID SETTINGS APPLY! โšก๐Ÿ’ฅ๐Ÿ”ฅ - settings.stashGridMode = tempStashGridMode - settings.stashMaxColumns = tempStashMaxColumns - settings.stashMaxRows = tempStashMaxRows - - - // Update preview and other UI components - ScreenshotApp.sharedInstance?.updatePreviewSize() - - // ๐ŸŽฏ NEW: Restore thumbnail if settings changes may have closed it - ScreenshotApp.sharedInstance?.restoreCurrentThumbnailIfNeeded() - - print("SettingsTabView: All changes applied successfully") - } - - private func resetToCurrentSettings() { - let settings = SettingsManager.shared - let timerVal = settings.thumbnailTimer - - tempThumbnailTimer = (timerVal == 0) ? 31.0 : max(5.0, Double(timerVal)) - tempFilenamePrefix = settings.filenamePrefix - tempFilenameFormatPreset = settings.filenameFormatPreset - tempFilenameCustomFormat = settings.filenameCustomFormat - tempThumbnailFixedSize = settings.thumbnailFixedSize - tempShowFolderButton = settings.showFolderButton - tempCloseAfterDrag = settings.closeAfterDrag - tempCloseAfterSave = settings.closeAfterSave - tempPlaySoundOnCapture = settings.playSoundOnCapture - tempScreenshotSoundVolume = settings.screenshotSoundVolume - tempScreenshotSoundType = settings.screenshotSoundType - tempPreferredBackgroundRemovalMethod = settings.preferredBackgroundRemovalMethod - tempCacheRetentionTime = settings.cacheRetentionTime - tempAutoSaveScreenshot = settings.autoSaveScreenshot - tempActionOrder = settings.actionOrder - tempIsRenameActionEnabled = settings.isRenameActionEnabled - tempIsStashActionEnabled = settings.isStashActionEnabled - tempIsOCRActionEnabled = settings.isOCRActionEnabled - tempIsClipboardActionEnabled = settings.isClipboardActionEnabled - tempIsBackgroundRemoveActionEnabled = settings.isBackgroundRemoveActionEnabled - tempIsCancelActionEnabled = settings.isCancelActionEnabled - tempIsRemoveActionEnabled = settings.isRemoveActionEnabled - tempScreenshotFolder = settings.screenshotFolder - tempThumbnailDisplayScreen = settings.thumbnailDisplayScreen - tempStashAlwaysOnTop = settings.stashAlwaysOnTop - tempPersistentStash = settings.persistentStash - tempHideDesktopIconsDuringScreenshot = settings.hideDesktopIconsDuringScreenshot - tempHideDesktopWidgetsDuringScreenshot = settings.hideDesktopWidgetsDuringScreenshot - - // ๐Ÿ”„ UPDATE SETTINGS RESET - tempStashPreviewSize = settings.stashPreviewSize - // ๐Ÿ”ฅ๐Ÿ’ฅโšก HYPERMODE GRID RESET TO CURRENT! โšก๐Ÿ’ฅ๐Ÿ”ฅ - tempStashGridMode = settings.stashGridMode - tempStashMaxColumns = settings.stashMaxColumns - tempStashMaxRows = settings.stashMaxRows - folderPathDisplay = settings.screenshotFolder ?? "Not selected" - - } - - private func formatTimerDisplay(_ value: Double) -> String { - if value >= 31 { // 31 and above represents "No timeout" - return "No timeout" - } else { - return "\(Int(value))s" - } - } - - private func getAvailableScreenOptions() -> [ThumbnailDisplayScreen] { - let screenCount = NSScreen.screens.count - var options: [ThumbnailDisplayScreen] = [.automatic] - - // Add screen options based on available screens (up to 5 screens) - if screenCount >= 1 { options.append(.screen1) } - if screenCount >= 2 { options.append(.screen2) } - if screenCount >= 3 { options.append(.screen3) } - if screenCount >= 4 { options.append(.screen4) } - if screenCount >= 5 { options.append(.screen5) } - - return options - } - - private func getScreenDisplayName(for option: ThumbnailDisplayScreen) -> String { - switch option { - case .automatic: - return "Automatic (where mouse is)" - case .screen1, .screen2, .screen3, .screen4, .screen5: - let screenIndex: Int - switch option { - case .screen1: screenIndex = 0 - case .screen2: screenIndex = 1 - case .screen3: screenIndex = 2 - case .screen4: screenIndex = 3 - case .screen5: screenIndex = 4 - default: screenIndex = 0 - } - - if screenIndex < NSScreen.screens.count { - let screen = NSScreen.screens[screenIndex] - let screenName = screen.localizedName - return option.getDisplayName(for: screenIndex, screenName: screenName) - } else { - return option.description + " (not available)" - } - } - } - - private func updateFilenameExample() -> String { - let prefix = tempFilenamePrefix - let preset = tempFilenameFormatPreset - var example = prefix - let now = Date() - let dateFormatter = DateFormatter() - - switch preset { - case .macOSStyle: - dateFormatter.dateFormat = "yyyy-MM-dd HH.mm.ss" - let dateString = dateFormatter.string(from: now) - example += (prefix.isEmpty ? "" : " ") + "at " + dateString - case .compactDateTime: - dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss" - example += (prefix.isEmpty ? "" : "_") + dateFormatter.string(from: now) - case .superCompactDateTime: - dateFormatter.dateFormat = "yyyyMMdd_HHmmss" - example += (prefix.isEmpty ? "" : "_") + dateFormatter.string(from: now) - case .timestamp: - example += (prefix.isEmpty ? "" : "_") + String(Int(now.timeIntervalSince1970)) - case .prefixOnly: - if example.isEmpty { example = "filename" } - case .custom: - var custom = tempFilenameCustomFormat - // Simple replacement for preview, the real logic is in ScreenshotApp - dateFormatter.dateFormat = "yyyy"; custom = custom.replacingOccurrences(of: "{YYYY}", with: dateFormatter.string(from: now)) - dateFormatter.dateFormat = "yy"; custom = custom.replacingOccurrences(of: "{YY}", with: dateFormatter.string(from: now)) - dateFormatter.dateFormat = "MM"; custom = custom.replacingOccurrences(of: "{MM}", with: dateFormatter.string(from: now)) - dateFormatter.dateFormat = "dd"; custom = custom.replacingOccurrences(of: "{DD}", with: dateFormatter.string(from: now)) - dateFormatter.dateFormat = "HH"; custom = custom.replacingOccurrences(of: "{hh}", with: dateFormatter.string(from: now)) - dateFormatter.dateFormat = "mm"; custom = custom.replacingOccurrences(of: "{mm}", with: dateFormatter.string(from: now)) - dateFormatter.dateFormat = "ss"; custom = custom.replacingOccurrences(of: "{ss}", with: dateFormatter.string(from: now)) - dateFormatter.dateFormat = "SSS"; custom = custom.replacingOccurrences(of: "{ms}", with: dateFormatter.string(from: now)) - if custom.isEmpty && prefix.isEmpty { example = "custom_example" } - else { example += (prefix.isEmpty || custom.starts(with: ["_", "-", " "]) ? "" : "_") + custom } - } - return example + ".png" - } - - // MARK: - AI Model Management - private static func getModelPath() -> String { - // Use Application Support directory instead of app bundle (which is read-only) - let appSupportURL = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! - let appDirectory = appSupportURL.appendingPathComponent("ShotScreen") - - // Create directory if it doesn't exist - try? FileManager.default.createDirectory(at: appDirectory, withIntermediateDirectories: true) - - return appDirectory.appendingPathComponent("bria-rmbg-coreml.mlpackage").path - } - - private func getModelPath() -> String { - return Self.getModelPath() - } - - private static func isModelInstalled() -> Bool { - return FileManager.default.fileExists(atPath: getModelPath()) - } - - private func isModelInstalled() -> Bool { - return Self.isModelInstalled() - } - - private func downloadModel() { - guard !isDownloadingModel else { return } - - isDownloadingModel = true - downloadProgress = 0.0 - - let downloadURL = AppConfig.modelDownloadURL - - guard let url = URL(string: downloadURL) else { - print("โŒ Invalid download URL") - isDownloadingModel = false - return - } - - // Create secure URLRequest with security headers - var request = URLRequest(url: url) - - // Add all security headers - for (key, value) in AppConfig.secureHeaders { - request.setValue(value, forHTTPHeaderField: key) - } - - // Set secure connection properties - request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData - request.timeoutInterval = 60.0 - - // Try two approaches: downloadTask first, then dataTask as fallback - print("๐Ÿš€ Starting model download with downloadTask approach...") - - let task = URLSession.shared.downloadTask(with: request) { [self] tempURL, response, error in - // Process the file IMMEDIATELY in the completion handler (not async) - var permanentURL: URL? - - if let error = error { - print("โŒ DownloadTask failed, trying dataTask approach: \(error.localizedDescription)") - DispatchQueue.main.async { - self.downloadModelWithDataTask(url: url) - } - return - } - - guard let tempURL = tempURL else { - print("โŒ No temporary file URL, trying dataTask approach") - DispatchQueue.main.async { - self.downloadModelWithDataTask(url: url) - } - return - } - - // SYNCHRONOUSLY copy het tijdelijke bestand BEFORE going to main queue - do { - let permanentLocation = FileManager.default.temporaryDirectory - .appendingPathComponent("rmbg_download_\(UUID().uuidString).tmp") - - try FileManager.default.copyItem(at: tempURL, to: permanentLocation) - permanentURL = permanentLocation - print("๐Ÿ“‹ Copied temp file to permanent location: \(permanentLocation.path)") - - } catch { - print("โŒ Failed to copy temp file IMMEDIATELY: \(error.localizedDescription)") - print("๐Ÿ”„ Trying dataTask approach as fallback...") - DispatchQueue.main.async { - self.downloadModelWithDataTask(url: url) - } - return - } - - // Now go to main queue with the permanent file - DispatchQueue.main.async { - self.isDownloadingModel = false - - if let permanentURL = permanentURL { - self.installModel(from: permanentURL) - } - } - } - - // Progress tracking - let _ = task.progress.observe(\.fractionCompleted) { progress, _ in - DispatchQueue.main.async { - self.downloadProgress = progress.fractionCompleted - } - } - - task.resume() - } - - // MARK: - Security File Validation - private func validateDownloadedFile(at url: URL, fileSize: Int64) -> Bool { - print("๐Ÿ”’ SECURITY: Validating downloaded file...") - - // ๐Ÿค– Special handling for ML models (less strict validation) - if isMLModelDownload(fileName: url.lastPathComponent, fileSize: fileSize) { - return validateMLModel(at: url, fileSize: fileSize) - } - - // 1. File size validation (between 1MB and 200MB) - let minSize: Int64 = 1 * 1024 * 1024 // 1MB minimum - let maxSize: Int64 = AppConfig.maxDownloadSize // 200MB maximum - - if fileSize < minSize { - print("โŒ SECURITY: File too small (\(fileSize) bytes) - possible malicious file") - return false - } - - if fileSize > maxSize { - print("โŒ SECURITY: File too large (\(fileSize) bytes) - exceeds safe limit") - return false - } - - // 2. File extension validation (basic check) - let fileName = url.lastPathComponent.lowercased() - let allowedExtensions = ["zip", "mlpackage", "tmp"] - let hasValidExtension = allowedExtensions.contains { fileName.hasSuffix($0) } - - if !hasValidExtension { - print("โŒ SECURITY: Suspicious file extension: \(fileName)") - return false - } - - // 3. Magic byte validation - do { - let fileHandle = try FileHandle(forReadingFrom: url) - defer { fileHandle.closeFile() } - - let headerData = fileHandle.readData(ofLength: 16) - - // Check for known safe file types - if let isValid = validateFileHeader(headerData, fileName: fileName) { - if !isValid { - print("โŒ SECURITY: Invalid file header for type") - return false - } - } - - // 4. Basic content scan for suspicious patterns - if !performBasicContentScan(fileHandle: fileHandle, fileName: fileName) { - return false - } - - } catch { - print("โŒ SECURITY: Could not read file for validation: \(error)") - return false - } - - print("โœ… SECURITY: File validation passed") - return true - } - - // MARK: - ML Model Specific Validation (Less Strict) - private func isMLModelDownload(fileName: String, fileSize: Int64) -> Bool { - let lowerFileName = fileName.lowercased() - - // Check for known ML model patterns - let mlModelPatterns = [ - "bria-rmbg", "rmbg", "coreml", "mlpackage", - "model", "neural", "ai" - ] - - // Check if filename contains ML model indicators - let isMLModel = mlModelPatterns.contains { pattern in - lowerFileName.contains(pattern) - } - - // Check if file size is in reasonable ML model range (10MB - 150MB) - let mlModelMinSize: Int64 = 10 * 1024 * 1024 // 10MB - let mlModelMaxSize: Int64 = 150 * 1024 * 1024 // 150MB - let isSizeReasonable = fileSize >= mlModelMinSize && fileSize <= mlModelMaxSize - - return isMLModel && isSizeReasonable - } - - private func validateMLModel(at url: URL, fileSize: Int64) -> Bool { - print("๐Ÿค– DEBUG: ML Model validation starting (less strict)...") - print("๐Ÿค– DEBUG: File: \(url.lastPathComponent), Size: \(fileSize) bytes") - - // 1. Size validation for ML models (10MB - 150MB) - let mlModelMinSize: Int64 = 10 * 1024 * 1024 // 10MB - let mlModelMaxSize: Int64 = 150 * 1024 * 1024 // 150MB - - if fileSize < mlModelMinSize { - print("โŒ DEBUG: ML model too small (\(fileSize) bytes, min: \(mlModelMinSize))") - return false - } - - if fileSize > mlModelMaxSize { - print("โŒ DEBUG: ML model too large (\(fileSize) bytes, max: \(mlModelMaxSize))") - return false - } - - print("โœ… DEBUG: ML model size validation passed (\(fileSize) bytes)") - - // 2. Basic file header check (allow ZIP or unknown formats) - do { - let fileHandle = try FileHandle(forReadingFrom: url) - defer { fileHandle.closeFile() } - - let headerData = fileHandle.readData(ofLength: 16) - print("๐Ÿ” DEBUG: ML model header: \(headerData.map { String(format: "0x%02X", $0) }.joined(separator: " "))") - - // For ML models, be more permissive with headers - if headerData.count >= 2 { - // Allow ZIP files (most common for ML models) - if headerData[0] == 0x50 && headerData[1] == 0x4B { - print("โœ… DEBUG: ML model ZIP format detected") - return true - } - - // Allow other binary formats (MLPackage can have various headers) - print("โ„น๏ธ DEBUG: ML model with unknown header - allowing (likely MLPackage)") - return true - } - - } catch { - print("โš ๏ธ DEBUG: Could not read ML model header (allowing anyway): \(error)") - // For ML models, be permissive if we can't read the header - return true - } - - print("โœ… DEBUG: ML model validation passed") - return true - } - - private func validateFileHeader(_ headerData: Data, fileName: String) -> Bool? { - guard headerData.count >= 4 else { return nil } - - // ZIP file validation (PK signature) - if headerData[0] == 0x50 && headerData[1] == 0x4B { - // Valid ZIP signatures: PK\x03\x04, PK\x05\x06, PK\x07\x08 - let signature = (UInt16(headerData[2]) << 8) | UInt16(headerData[3]) - let validZipSignatures: [UInt16] = [0x0304, 0x0506, 0x0708] - - if validZipSignatures.contains(signature) { - print("โœ… SECURITY: Valid ZIP file header") - return true - } else { - print("โŒ SECURITY: Invalid ZIP signature") - return false - } - } - - // For .mlpackage (usually a directory, but could be packed differently) - if fileName.contains("mlpackage") { - // MLPackage can have various formats, allow more flexibility - print("โ„น๏ธ SECURITY: MLPackage file - allowing flexible header") - return true - } - - // Unknown file type - allow but log - print("โš ๏ธ SECURITY: Unknown file type - proceeding with caution") - return nil - } - - private func performBasicContentScan(fileHandle: FileHandle, fileName: String) -> Bool { - print("๐Ÿ” SECURITY: Performing basic content scan...") - - // ๐Ÿค– Skip intensive scanning for ML models (they naturally have high entropy) - let lowerFileName = fileName.lowercased() - let isMLModel = ["bria-rmbg", "rmbg", "coreml", "mlpackage", "model"].contains { pattern in - lowerFileName.contains(pattern) - } - - if isMLModel { - print("๐Ÿค– SECURITY: Skipping intensive content scan for ML model") - return true - } - - // Read a larger sample for content analysis (first 1KB) - fileHandle.seek(toFileOffset: 0) - let sampleData = fileHandle.readData(ofLength: 1024) - - // Convert to string for text analysis (if possible) - if let sampleText = String(data: sampleData, encoding: .utf8) { - // Check for suspicious script patterns - let suspiciousPatterns = [ - "#!/bin/sh", "#!/bin/bash", "cmd.exe", "powershell", - " 7.5 { // High entropy could indicate encryption/obfuscation - print("โš ๏ธ SECURITY: High entropy detected (\(String(format: "%.2f", entropy))) - possible obfuscated content") - // Don't fail on high entropy alone, just log it - } - - print("โœ… SECURITY: Content scan passed") - return true - } - - private func calculateEntropy(data: Data) -> Double { - var frequency = [UInt8: Int]() - - // Count byte frequencies - for byte in data { - frequency[byte, default: 0] += 1 - } - - // Calculate Shannon entropy - let dataLength = Double(data.count) - var entropy: Double = 0.0 - - for count in frequency.values { - let probability = Double(count) / dataLength - if probability > 0 { - entropy -= probability * log2(probability) - } - } - - return entropy - } - - private func downloadModelWithDataTask(url: URL) { - print("๐Ÿ”„ Falling back to dataTask approach...") - - // Reset progress for new attempt - downloadProgress = 0.0 - - var request = URLRequest(url: url) - - // Add all security headers - for (key, value) in AppConfig.secureHeaders { - request.setValue(value, forHTTPHeaderField: key) - } - - // Set secure connection properties - request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData - request.timeoutInterval = 60.0 - - let task = URLSession.shared.dataTask(with: request) { [self] data, response, error in - DispatchQueue.main.async { - self.isDownloadingModel = false - - if let error = error { - print("โŒ DataTask download error: \(error.localizedDescription)") - self.downloadProgress = 0.0 - return - } - - guard let data = data else { - print("โŒ No data received") - self.downloadProgress = 0.0 - return - } - - print("๐Ÿ“Š Downloaded data size: \(data.count) bytes") - - // Write data to temporary file - do { - let tempURL = FileManager.default.temporaryDirectory - .appendingPathComponent("rmbg_datadownload_\(UUID().uuidString).tmp") - - try data.write(to: tempURL) - print("๐Ÿ“ Wrote data to temp file: \(tempURL.path)") - - self.installModel(from: tempURL) - } catch { - print("โŒ Failed to write downloaded data: \(error.localizedDescription)") - self.downloadProgress = 0.0 - } - } - } - - task.resume() - } - - private func installModel(from tempURL: URL) { - do { - print("๐Ÿ” DEBUG: Starting model installation from: \(tempURL.path)") - - // Check if the file exists and get its size - let fileAttributes = try FileManager.default.attributesOfItem(atPath: tempURL.path) - let fileSize = fileAttributes[.size] as? Int64 ?? 0 - print("๐Ÿ“Š DEBUG: Downloaded file size: \(fileSize) bytes") - - // ๐Ÿ”’ SECURITY: Basic file validation - print("๐Ÿ”’ DEBUG: Starting file validation...") - if !validateDownloadedFile(at: tempURL, fileSize: fileSize) { - print("โŒ DEBUG: File validation failed - removing malicious file") - try? FileManager.default.removeItem(at: tempURL) - DispatchQueue.main.async { - self.isDownloadingModel = false - self.downloadProgress = 0.0 - } - return - } - print("โœ… DEBUG: File validation passed") - - let destinationPath = getModelPath() - let destinationURL = URL(fileURLWithPath: destinationPath) - - // Remove existing model if present - if FileManager.default.fileExists(atPath: destinationPath) { - try FileManager.default.removeItem(at: destinationURL) - } - - // Check if the downloaded file is a zip archive - let fileHandle = try FileHandle(forReadingFrom: tempURL) - let header = fileHandle.readData(ofLength: 4) - fileHandle.closeFile() - - // ZIP file magic numbers: PK\x03\x04 or PK\x05\x06 or PK\x07\x08 - let isPKZip = header.count >= 2 && header[0] == 0x50 && header[1] == 0x4B - - print("๐Ÿ” DEBUG: isPKZip = \(isPKZip), header = \(header.map { String(format: "0x%02X", $0) }.joined(separator: " "))") - - if isPKZip { - print("๐Ÿ“ฆ DEBUG: File appears to be a ZIP archive, extracting...") - - // Create temporary directory for extraction - let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) - print("๐Ÿ“ DEBUG: Created temp extraction directory: \(tempDir.path)") - - // Unzip the downloaded file - let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/unzip") - process.arguments = ["-q", tempURL.path, "-d", tempDir.path] - - try process.run() - process.waitUntilExit() - - print("๐Ÿ“ฆ DEBUG: Unzip exit code: \(process.terminationStatus)") - - if process.terminationStatus != 0 { - print("โŒ DEBUG: Failed to unzip model with exit code \(process.terminationStatus)") - return - } - - // Find the .mlpackage file - let contents = try FileManager.default.contentsOfDirectory(at: tempDir, includingPropertiesForKeys: nil) - print("๐Ÿ“ DEBUG: Extracted contents: \(contents.map { $0.lastPathComponent })") - - guard let mlpackageURL = contents.first(where: { $0.pathExtension == "mlpackage" || $0.lastPathComponent.contains("bria-rmbg-coreml") }) else { - print("โŒ DEBUG: No .mlpackage found in downloaded file") - print("๐Ÿ“ DEBUG: All contents: \(contents.map { "\($0.lastPathComponent) (ext: \($0.pathExtension))" })") - // Cleanup temp directory - try? FileManager.default.removeItem(at: tempDir) - return - } - - print("๐Ÿ“ฆ DEBUG: Found mlpackage: \(mlpackageURL.lastPathComponent)") - - // Copy the extracted model - try FileManager.default.copyItem(at: mlpackageURL, to: destinationURL) - print("๐Ÿ“ฆ DEBUG: Successfully copied model to: \(destinationURL.path)") - - // Cleanup - try FileManager.default.removeItem(at: tempDir) - print("๐Ÿ—‘ DEBUG: Cleaned up temp directory") - - } else { - print("๐Ÿ“ฑ DEBUG: File appears to be a direct .mlpackage, copying directly...") - - // Assume it's already a .mlpackage directory and copy directly - try FileManager.default.copyItem(at: tempURL, to: destinationURL) - print("๐Ÿ“ฑ DEBUG: Successfully copied direct model to: \(destinationURL.path)") - } - - print("โœ… DEBUG: Model installed successfully") - - // Check if model actually exists at destination - let finalModelExists = FileManager.default.fileExists(atPath: destinationPath) - print("๐Ÿ” DEBUG: Model exists at destination: \(finalModelExists) (\(destinationPath))") - - // Cleanup: Remove the downloaded temp file - try? FileManager.default.removeItem(at: tempURL) - print("๐Ÿ—‘ DEBUG: Cleaned up downloaded temp file") - - // Refresh the UI to show the model is now available - DispatchQueue.main.async { - // Update the local state to refresh the Settings UI - print("๐Ÿ”„ DEBUG: Updating UI state - setting isRMBGModelInstalled = true") - self.isRMBGModelInstalled = true - self.isDownloadingModel = false - self.downloadProgress = 0.0 - - // Notify background remove windows that model is now available - NotificationCenter.default.post( - name: NSNotification.Name("RMBGModelDownloadCompleted"), - object: nil - ) - print("๐Ÿ“ก DEBUG: Sent RMBGModelDownloadCompleted notification") - } - - } catch { - print("โŒ DEBUG: Installation error: \(error.localizedDescription)") - print("โŒ DEBUG: Full error: \(error)") - - // Cleanup: Remove the downloaded temp file on error - try? FileManager.default.removeItem(at: tempURL) - print("๐Ÿ—‘ DEBUG: Cleaned up temp file after error") - - // Reset download state on error - DispatchQueue.main.async { - print("๐Ÿ”„ DEBUG: Resetting download state due to error") - self.isDownloadingModel = false - self.downloadProgress = 0.0 - } - } - } - - private func removeModel() { - let modelPath = getModelPath() - - do { - if FileManager.default.fileExists(atPath: modelPath) { - try FileManager.default.removeItem(atPath: modelPath) - print("โœ… Model removed successfully") - - // Update UI state - DispatchQueue.main.async { - self.isRMBGModelInstalled = false - - // Notify other parts of the app that model was removed - NotificationCenter.default.post( - name: NSNotification.Name("RMBGModelRemoved"), - object: nil - ) - } - } else { - print("โš ๏ธ Model file not found at path: \(modelPath)") - } - } catch { - print("โŒ Error removing model: \(error.localizedDescription)") - } - } - - var body: some View { - VStack(spacing: 0) { - ScrollView { - VStack(spacing: 0) { - // Add minimal top padding since traffic lights are hidden - Spacer() - .frame(height: 20) - - VStack(spacing: 24) { - // Title Section - VStack(spacing: 8) { - Text("ShotScreen Settings") - .font(.largeTitle) - .fontWeight(.bold) - .foregroundColor(.primary) - .multilineTextAlignment(.center) - - Text("Configure your screenshot experience") - .font(.subheadline) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - } - .padding(.top, 10) - .padding(.bottom, 20) - - // Thumbnail Section - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 8) { - Text("Thumbnail") - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.primary) - Divider() - } - HStack { - Text("Visibility duration:") - Slider(value: $tempThumbnailTimer, in: 5...31, step: 1) { - Text("") - } - Text(formatTimerDisplay(tempThumbnailTimer)) - .frame(width: 80, alignment: .trailing) - } - - HStack { - Text("Thumbnail size:") - .frame(width: 110, alignment: .leading) - Picker("", selection: $tempThumbnailFixedSize) { - ForEach(ThumbnailFixedSize.allCases, id: \.self) { size in - Text(size.displayName).tag(size) - } - } - .labelsHidden() - } - - HStack { - Text("Show on screen:") - .frame(width: 110, alignment: .leading) - Picker("", selection: $tempThumbnailDisplayScreen) { - ForEach(getAvailableScreenOptions(), id: \.self) { option in - Text(getScreenDisplayName(for: option)).tag(option) - } - } - .labelsHidden() - } - - Toggle("Close thumbnail after dragging", isOn: $tempCloseAfterDrag) - Toggle("Close thumbnail after saving", isOn: $tempCloseAfterSave) - - Divider() - - // Storage Subsection - VStack(alignment: .leading, spacing: 12) { - Text("Storage") - .font(.headline) - .foregroundColor(.primary) - - // Folder settings - HStack { - TextField("Folder path", text: .constant(folderPathDisplay)) - .disabled(true) - Button("Choose...") { - let panel = NSOpenPanel() - panel.canChooseDirectories = true - panel.canChooseFiles = false - panel.allowsMultipleSelection = false - panel.message = "Select folder to save screenshots" - panel.prompt = "Choose" - if panel.runModal() == .OK, let url = panel.url { - tempScreenshotFolder = url.path - folderPathDisplay = url.path - } - } - } - - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Save Mode:") - .frame(width: 110, alignment: .leading) - Picker("", selection: Binding( - get: { tempAutoSaveScreenshot ? ScreenshotSaveMode.automatic : ScreenshotSaveMode.manual }, - set: { tempAutoSaveScreenshot = ($0 == .automatic) } - )) { - ForEach(ScreenshotSaveMode.allCases) { mode in - Text(mode.description).tag(mode) - } - } - .labelsHidden() - } - - // Warning text voor manual mode - if !tempAutoSaveScreenshot { - Text("Recommending to set timer to No Timeout, otherwise the screenshot will close and is lost") - .font(.caption) - .foregroundColor(.red) - .padding(.top, 4) - } - } - - Toggle("Show folder button in thumbnail", isOn: $tempShowFolderButton) - } - - Divider() - - // Cache Management Subsection - VStack(alignment: .leading, spacing: 12) { - Text("Cache Management") - .font(.headline) - .foregroundColor(.primary) - - // Cache info display - VStack(alignment: .leading, spacing: 6) { - HStack { - Text("Cache size:") - .frame(width: 110, alignment: .leading) - Text("\(String(format: "%.1f", cacheSize)) MB (\(cacheFileCount) files)") - .foregroundColor(.secondary) - .font(.caption) - - Spacer() - - Button("Open Folder") { - openCacheFolder() - } - .buttonStyle(.bordered) - .controlSize(.small) - - Button("Refresh") { - updateCacheInfo() - } - .buttonStyle(.bordered) - .controlSize(.small) - } - - VStack(alignment: .leading, spacing: 2) { - Text("Cache contains temporary screenshots for thumbnail restoration and recovery") - .font(.caption) - .foregroundColor(.secondary) - } - } - - // Cache retention time setting - HStack { - Text("Keep for:") - .frame(width: 110, alignment: .leading) - Picker("", selection: $tempCacheRetentionTime) { - ForEach(CacheRetentionTime.allCases, id: \.self) { time in - Text(time.displayName).tag(time) - } - } - .labelsHidden() - } - - // Clear cache button - HStack { - Button("Clear Cache") { - showClearCacheConfirmation = true - } - .buttonStyle(.bordered) - .foregroundColor(.red) - - Spacer() - - Text("Removes old screenshots but preserves active thumbnails") - .font(.caption) - .foregroundColor(.secondary) - } - } - } - .padding(20) - .background(Color.primary.opacity(0.03)) - .cornerRadius(12) - .onAppear { - updateCacheInfo() - } - - // Screenshot Section - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 8) { - Text("Screenshot") - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.primary) - Divider() - } - Toggle("Hide desktop icons during screenshots", isOn: $tempHideDesktopIconsDuringScreenshot) - .help("When enabled, desktop icons will be temporarily hidden during screenshot capture for a cleaner look") - - Toggle("Hide desktop widgets during screenshots", isOn: $tempHideDesktopWidgetsDuringScreenshot) - .help("When enabled, desktop widgets (like weather widgets, clock widgets, etc.) will be temporarily hidden during screenshot capture for a cleaner look") - - Divider() - - VStack(alignment: .leading, spacing: 12) { - Text("Screenshot Shortcut") - .font(.headline) - .foregroundColor(.primary) - - Toggle("Use Custom Shortcut", isOn: $settings.useCustomShortcut) - - if settings.useCustomShortcut { - HStack { - Text("Custom Shortcut:") - .frame(minWidth: 120, alignment: .leading) - - ShortcutRecorder( - modifiers: $settings.customShortcutModifiers, - keyCode: $settings.customShortcutKey, - placeholder: "Click to record shortcut" - ) - .frame(height: 28) - } - - Text("Current: \(formatCurrentShortcut())") - .font(.caption) - .foregroundColor(.secondary) - } else { - HStack { - Text("Using Default:") - .frame(minWidth: 120, alignment: .leading) - Text("โŒ˜โ‡ง4") - .font(.system(size: 16, design: .monospaced)) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.gray.opacity(0.2)) - .cornerRadius(4) - } - } - - HStack { - Button("Open macOS Screenshot Settings") { - openMacOSScreenshotSettings() - } - .buttonStyle(.bordered) - - Spacer() - } - - Text("Note: Disable the macOS Cmd+Shift+4 shortcut in System Preferences to avoid conflicts.") - .font(.caption) - .foregroundColor(.secondary) - } - - Divider() - - VStack(alignment: .leading, spacing: 8) { - Text("Screenshot Modes") - .font(.headline) - .foregroundColor(.primary) - - let currentShortcut = getCurrentShortcutDisplay() - - Text("โ€ข \(currentShortcut) once: Drag to select area or click for full screen") - .font(.caption) - .foregroundColor(.secondary) - Text("โ€ข \(currentShortcut) twice: Capture whole screen under cursor") - .font(.caption) - .foregroundColor(.secondary) - Text("โ€ข \(currentShortcut) three times: Capture all screens combined") - .font(.caption) - .foregroundColor(.secondary) - Text("โ€ข During selection: Press Space for window capture") - .font(.caption) - .foregroundColor(.secondary) - } - } - .padding(20) - .background(Color.primary.opacity(0.03)) - .cornerRadius(12) - - // Filename Format Section - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 8) { - Text("Filename Format") - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.primary) - Divider() - } - TextField("Prefix:", text: $tempFilenamePrefix) - HStack { - Text("Format Preset:") - .frame(width: 110, alignment: .leading) - Picker("", selection: $tempFilenameFormatPreset) { - ForEach(FilenameFormatPreset.allCases, id: \.self) { preset in - Text(preset.description).tag(preset) - } - } - .labelsHidden() - } - - if tempFilenameFormatPreset == .custom { - TextField("Custom Format:", text: $tempFilenameCustomFormat) - Text("Use: {YYYY}, {MM}, {DD}, {hh}, {mm}, {ss}, {ms}") - .font(.caption) - .foregroundColor(.gray) - } - Text("Example: \(updateFilenameExample())") - .font(.caption) - .foregroundColor(.gray) - } - .padding() - .background(Color.primary.opacity(0.03)) - .cornerRadius(12) - - // Action Bar Section - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Action Bar") - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.primary) - - Spacer() - - Button("Reset order") { - tempActionOrder = ActionType.allCases - } - .buttonStyle(.bordered) - } - Divider() - } - - ForEach(tempActionOrder) { action in - HStack { - Toggle("\(action.displayName)", isOn: Binding( - get: { - switch action { - case .rename: return tempIsRenameActionEnabled - case .stash: return tempIsStashActionEnabled - case .ocr: return tempIsOCRActionEnabled - case .clipboard: return tempIsClipboardActionEnabled - case .backgroundRemove: return tempIsBackgroundRemoveActionEnabled - case .cancel: return tempIsCancelActionEnabled - case .remove: return tempIsRemoveActionEnabled - } - }, - set: { newValue in - switch action { - case .rename: tempIsRenameActionEnabled = newValue - case .stash: tempIsStashActionEnabled = newValue - case .ocr: tempIsOCRActionEnabled = newValue - case .clipboard: tempIsClipboardActionEnabled = newValue - case .backgroundRemove: tempIsBackgroundRemoveActionEnabled = newValue - case .cancel: tempIsCancelActionEnabled = newValue - case .remove: tempIsRemoveActionEnabled = newValue - } - } - )) - - Spacer() - - HStack(spacing: 4) { - Button(action: { - moveActionInTemp(action, direction: -1) - }) { - Image(systemName: "arrow.up") - .frame(width: 20, height: 20) - } - .buttonStyle(.borderless) - .disabled(tempActionOrder.first == action) - - Button(action: { - moveActionInTemp(action, direction: 1) - }) { - Image(systemName: "arrow.down") - .frame(width: 20, height: 20) - } - .buttonStyle(.borderless) - .disabled(tempActionOrder.last == action) - } - } - } - } - .padding() - .background(Color.primary.opacity(0.03)) - .cornerRadius(12) - - // Stash Section - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Stash") - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.primary) - - Spacer() - - Button("Show Stash") { - openStash() - } - .buttonStyle(.bordered) - } - Divider() - } - - VStack(alignment: .leading, spacing: 12) { - Text("Window Behavior") - .font(.headline) - .foregroundColor(.primary) - - Toggle("Always on top", isOn: $tempStashAlwaysOnTop) - .help("Keep the Stash window visible above all other applications") - - VStack(alignment: .leading, spacing: 4) { - Toggle("Persistent storage", isOn: $tempPersistentStash) - .help("Keep stash images available when you close and reopen the window") - - Text("When enabled, your stashed screenshots remain available after closing the Stash window. When disabled, stash is cleared each time you close it.") - .font(.caption) - .foregroundColor(.secondary) - } - } - - Divider() - - VStack(alignment: .leading, spacing: 12) { - Text("Grid Layout") - .font(.headline) - .foregroundColor(.primary) - - HStack { - Text("Layout direction:") - .frame(width: 110, alignment: .leading) - Picker("", selection: $tempStashGridMode) { - ForEach(StashGridMode.allCases, id: \.self) { mode in - Text(mode.displayName).tag(mode) - } - } - .labelsHidden() - } - - HStack { - Text(tempStashGridMode == .fixedColumns ? "Max columns:" : "Max rows:") - - Stepper(value: tempStashGridMode == .fixedColumns ? $tempStashMaxColumns : $tempStashMaxRows, - in: 1...5) { - Text("\(tempStashGridMode == .fixedColumns ? tempStashMaxColumns : tempStashMaxRows)") - .foregroundColor(.blue) - .frame(minWidth: 20) - } - } - - Text(tempStashGridMode == .fixedColumns ? - "Grid expands vertically as you add more screenshots" : - "Grid expands horizontally as you add more screenshots") - .font(.caption) - .foregroundColor(.secondary) - } - - Divider() - - VStack(alignment: .leading, spacing: 12) { - Text("Preview Settings") - .font(.headline) - .foregroundColor(.primary) - - HStack { - Text("Preview size:") - .frame(width: 110, alignment: .leading) - Picker("", selection: $tempStashPreviewSize) { - ForEach(StashPreviewSize.allCases, id: \.self) { size in - Text(size.displayName).tag(size) - } - } - .labelsHidden() - .help("Choose the size of preview windows when hovering over thumbnails") - } - } - } - .padding(20) - .background(Color.primary.opacity(0.03)) - .cornerRadius(12) - - // Background Removal Section - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 8) { - Text("Background Removal") - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.primary) - Divider() - } - VStack(alignment: .leading, spacing: 12) { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text("Vision Framework") - .font(.headline) - Text("Apple's built-in background removal - Always available") - .font(.caption) - .foregroundColor(.secondary) - } - Spacer() - - Text("โœ… Available") - .font(.caption) - .foregroundColor(.green) - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background(Color.green.opacity(0.1)) - .cornerRadius(8) - .frame(minWidth: 80) - } - - Divider() - - HStack { - VStack(alignment: .leading, spacing: 4) { - Text("RMBG-1.4") - .font(.headline) - Text("Better background removal tool - Additional download required") - .font(.caption) - .foregroundColor(.secondary) - } - Spacer() - - HStack(spacing: 8) { - if isRMBGModelInstalled { - Button(action: { - showRemoveConfirmation = true - }) { - Text("๐Ÿ—‘") - .font(.caption) - .foregroundColor(.red) - .padding(.horizontal, 8) - .padding(.vertical, 6) - .background(Color.red.opacity(0.1)) - .cornerRadius(8) - } - .buttonStyle(.borderless) - .help("Remove RMBG-1.4 model") - } - - if isRMBGModelInstalled { - Text("โœ… Available") - .font(.caption) - .foregroundColor(.green) - .padding(.horizontal, 12) - .padding(.vertical, 6) - .background(Color.green.opacity(0.1)) - .cornerRadius(8) - .frame(minWidth: 80) - } else { - Button(action: { - showDownloadConfirmation = true - }) { - if isDownloadingModel { - HStack(spacing: 6) { - ProgressView() - .scaleEffect(0.7) - Text("\(Int(downloadProgress * 100))%") - .font(.caption) - } - .padding(.horizontal, 12) - .padding(.vertical, 6) - .frame(minWidth: 80) - } else { - Text("๐Ÿ“ฅ Download") - .font(.caption) - .padding(.horizontal, 12) - .padding(.vertical, 6) - .frame(minWidth: 80) - } - } - .disabled(isDownloadingModel) - .buttonStyle(.borderedProminent) - } - } - } - - if isDownloadingModel { - ProgressView(value: downloadProgress) - .progressViewStyle(LinearProgressViewStyle()) - } - } - .padding(.vertical, 8) - - Divider() - - VStack(alignment: .leading, spacing: 12) { - Text("Preferred Method") - .font(.headline) - .foregroundColor(.primary) - - VStack(alignment: .leading, spacing: 8) { - ForEach(BackgroundRemovalMethod.allCases) { method in - HStack { - Button(action: { - tempPreferredBackgroundRemovalMethod = method - }) { - HStack { - Image(systemName: tempPreferredBackgroundRemovalMethod == method ? "largecircle.fill.circle" : "circle") - .foregroundColor(tempPreferredBackgroundRemovalMethod == method ? .blue : .secondary) - - VStack(alignment: .leading, spacing: 2) { - Text(method.displayName) - .font(.body) - .foregroundColor(.primary) - - Text(method.description) - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - .padding(.vertical, 4) - } - .buttonStyle(.plain) - .disabled(method == .rmbg && !isRMBGModelInstalled) - .opacity(method == .rmbg && !isRMBGModelInstalled ? 0.5 : 1.0) - } - - if method == .rmbg && !isRMBGModelInstalled { - Text("โš ๏ธ RMBG-1.4 model not installed - this option will not work") - .font(.caption) - .foregroundColor(.orange) - .padding(.leading, 24) - } - } - } - } - } - .padding() - .background(Color.primary.opacity(0.03)) - .cornerRadius(12) - - // Other Section - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 8) { - Text("Other") - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.primary) - Divider() - } - - VStack(alignment: .leading, spacing: 12) { - Text("Audio & System") - .font(.headline) - .foregroundColor(.primary) - - Toggle("Play sound on screenshot", isOn: $tempPlaySoundOnCapture) - - if tempPlaySoundOnCapture { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Sound type:") - .frame(width: 110, alignment: .leading) - Picker("", selection: $tempScreenshotSoundType) { - ForEach(ScreenshotSoundType.allCases, id: \.self) { soundType in - Text(soundType.displayName).tag(soundType) - } - } - .labelsHidden() - - Spacer() - - Button("Test") { - testSound() - } - .buttonStyle(.bordered) - .controlSize(.small) - } - - HStack { - Text("Volume:") - .frame(width: 110, alignment: .leading) - Slider(value: $tempScreenshotSoundVolume, in: 0.0...1.0, step: 0.05) - Text("\(Int(tempScreenshotSoundVolume * 100))%") - .frame(width: 40, alignment: .trailing) - .font(.caption) - .foregroundColor(.secondary) - } - } - .padding(.leading, 16) - .transition(.opacity.combined(with: .move(edge: .top))) - } - } - - VStack(alignment: .leading, spacing: 12) { - Text("Startup & Integration") - .font(.headline) - .foregroundColor(.primary) - - HStack { - VStack(alignment: .leading, spacing: 4) { - Text("Start ShotScreen automatically at login") - .font(.body) - Text("Configure ShotScreen to launch automatically when you log in to your Mac") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button("Setup Instructions") { - showLoginInstructionsPopup = true - } - .buttonStyle(.bordered) - } - } - } - .padding(20) - .background(Color.primary.opacity(0.03)) - .cornerRadius(12) - - // LICENSE SECTION - LicenseSection() - - // ๐Ÿ”„ UPDATES SECTION - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Updates") - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.primary) - - Spacer() - - Button("Check for Updates") { - if let app = NSApp.delegate as? ScreenshotApp { - app.checkForUpdates() - } - } - .buttonStyle(.bordered) - .disabled(!UpdateManager.shared.isUpdaterAvailable) - } - Divider() - } - - VStack(alignment: .leading, spacing: 12) { - Text("Current Version") - .font(.headline) - .foregroundColor(.primary) - - HStack { - Text("ShotScreen") - .font(.subheadline) - .foregroundColor(.secondary) - - Text(getCurrentVersionInfo()) - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(.primary) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(Color.blue.opacity(0.1)) - .cornerRadius(6) - - Spacer() - } - } - - Divider() - - VStack(alignment: .leading, spacing: 12) { - Text("Contact Developer") - .font(.headline) - .foregroundColor(.primary) - - HStack { - VStack(alignment: .leading, spacing: 4) { - Text("Need help or have feedback?") - .font(.body) - .foregroundColor(.primary) - - Text("Get in touch with questions, feature requests, or bug reports") - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - Button("Contact Developer") { - openDeveloperEmail() - } - .buttonStyle(.bordered) - } - - HStack { - Image(systemName: "envelope") - .foregroundColor(.secondary) - .frame(width: 16, height: 16) - - Text("info@shotscreen.app") - .font(.caption) - .foregroundColor(.secondary) - .textSelection(.enabled) - - Spacer() - } - } - - } - .padding(20) - .background(Color.primary.opacity(0.03)) - .cornerRadius(12) - - Spacer() - } - .padding() - } - } - - Divider() - // Save / Cancel buttons - HStack { - // "Apply" knop met visuele feedback - alleen actief als er wijzigingen zijn - Button(action: { - applyAllChanges() - - // Start animatie - withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) { - showApplyConfirmation = true - } - // Na 1,2 s verdwijnt de animatie weer - DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { - withAnimation(.easeOut) { - showApplyConfirmation = false - } - } - }) { - if showApplyConfirmation { - Label("Applied", systemImage: "checkmark.circle.fill") - .foregroundColor(.green) - .transition(.scale.combined(with: .opacity)) - } else { - Text("Apply") - } - } - .keyboardShortcut(.defaultAction) // Return-toets = Apply - .disabled(!hasUnsavedChanges) // Alleen actief als er wijzigingen zijn - - Spacer() - - Button("Cancel") { - resetToCurrentSettings() - } - .disabled(!hasUnsavedChanges) // Alleen actief als er wijzigingen zijn - - Button("Close") { - if hasUnsavedChanges && !forceClose { - showUnsavedChangesAlert = true - } else { - closeWindow() - } - } - } - .padding() - } - .frame(minWidth: 550, idealWidth: 650, minHeight: 800, idealHeight: 800) - .alert("Unsaved Changes", isPresented: $showUnsavedChangesAlert) { - Button("Save & Close") { - applyAllChanges() - forceClose = true - DispatchQueue.main.async { - closeWindow() - } - } - Button("Don't Save", role: .destructive) { - resetToCurrentSettings() - forceClose = true - DispatchQueue.main.async { - closeWindow() - } - } - Button("Cancel", role: .cancel) { - // Do nothing, just dismiss alert - } - } message: { - Text("You have unsaved changes. Do you want to save them before closing?") - } - .alert("Download RMBG-1.4 Model", isPresented: $showDownloadConfirmation) { - Button("Download") { - downloadModel() - } - Button("Cancel", role: .cancel) { - // Do nothing - } - } message: { - Text("The RMBG-1.4 model is approximately 90MB in size. Do you want to download and install it?") - } - .alert("Remove RMBG-1.4 Model", isPresented: $showRemoveConfirmation) { - Button("Remove", role: .destructive) { - removeModel() - } - Button("Cancel", role: .cancel) { - // Do nothing - } - } message: { - Text("Are you sure you want to remove the RMBG-1.4 model? You'll need to download it again to use this feature.") - } - .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RMBGModelDownloadCompleted"))) { _ in - // Update the model status when notified from external sources - isRMBGModelInstalled = Self.isModelInstalled() - } - .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("RMBGModelRemoved"))) { _ in - // Update the model status when model is removed - isRMBGModelInstalled = false - } - // Toegevoegd .onReceive voor hideDesktopIconsSettingChanged - .onReceive(NotificationCenter.default.publisher(for: .hideDesktopIconsSettingChanged)) { _ in - print("SettingsTabView: Received hideDesktopIconsSettingChanged notification in backup.") - self.tempHideDesktopIconsDuringScreenshot = SettingsManager.shared.hideDesktopIconsDuringScreenshot - } - .sheet(isPresented: $showLoginInstructionsPopup) { - LoginInstructionsPopupView(openSettingsAction: openLoginItemsSettings) - } - .alert("Clear Cache", isPresented: $showClearCacheConfirmation) { - Button("Clear", role: .destructive) { - clearCache() - } - Button("Cancel", role: .cancel) { - // Do nothing - } - } message: { - Text("Are you sure you want to clear \(String(format: "%.1f", cacheSize)) MB of cached screenshots? Active thumbnails will be preserved.") - } - } - - // Helper to close the containing window - private func closeWindow() { - NSApp.keyWindow?.close() - } - - // NIEUW: Helper methods for shortcut management - private func formatCurrentShortcut() -> String { - let modifiers = settings.customShortcutModifiers - let keyCode = settings.customShortcutKey - - if modifiers == 0 && keyCode == 0 { - return "No shortcut set" - } - - var parts: [String] = [] - - if modifiers & (1 << 3) != 0 { parts.append("โŒƒ") } // Control - if modifiers & (1 << 2) != 0 { parts.append("โŒฅ") } // Option - if modifiers & (1 << 1) != 0 { parts.append("โ‡ง") } // Shift - if modifiers & (1 << 0) != 0 { parts.append("โŒ˜") } // Command - - if let keyName = keyCodeToString(keyCode) { - parts.append(keyName) - } - - return parts.joined() - } - - private func keyCodeToString(_ keyCode: UInt16) -> String? { - // Same mapping as in ShortcutRecorderView - switch keyCode { - case 0: return "A" - case 1: return "S" - case 2: return "D" - case 3: return "F" - case 4: return "H" - case 5: return "G" - case 6: return "Z" - case 7: return "X" - case 8: return "C" - case 9: return "V" - case 11: return "B" - case 12: return "Q" - case 13: return "W" - case 14: return "E" - case 15: return "R" - case 16: return "Y" - case 17: return "T" - case 18: return "1" - case 19: return "2" - case 20: return "3" - case 21: return "4" - case 22: return "6" - case 23: return "5" - case 24: return "=" - case 25: return "9" - case 26: return "7" - case 27: return "-" - case 28: return "8" - case 29: return "0" - case 30: return "]" - case 31: return "O" - case 32: return "U" - case 33: return "[" - case 34: return "I" - case 35: return "P" - case 37: return "L" - case 38: return "J" - case 39: return "'" - case 40: return "K" - case 41: return ";" - case 42: return "\\" - case 43: return "," - case 44: return "/" - case 45: return "N" - case 46: return "M" - case 47: return "." - case 50: return "`" - case 36: return "โ†ฉ" // Return - case 48: return "โ‡ฅ" // Tab - case 49: return "Space" - case 51: return "โŒซ" // Delete - case 53: return "โŽ‹" // Escape - case 122: return "F1" - case 120: return "F2" - case 99: return "F3" - case 118: return "F4" - case 96: return "F5" - case 97: return "F6" - case 98: return "F7" - case 100: return "F8" - case 101: return "F9" - case 109: return "F10" - case 103: return "F11" - case 111: return "F12" - default: return "\(keyCode)" - } - } - - private func openMacOSScreenshotSettings() { - // Open macOS Keyboard > Screenshots settings - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.keyboard?Shortcuts") { - NSWorkspace.shared.open(url) - } - } - - private func openStash() { - // Open Stash window via the main app delegate - if let app = NSApp.delegate as? ScreenshotApp { - app.showStash(nil) - } - } - - private func openLoginItemsSettings() { - print("๐Ÿ”ง Opening Login Items Settings...") - - // Voor macOS 13+ - gebruik eenvoudige URL - if #available(macOS 13.0, *) { - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.users") { - NSWorkspace.shared.open(url) - return - } - } - - // Voor macOS 12 en ouder - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.users") { - NSWorkspace.shared.open(url) - } else { - print("โŒ Could not open Login Items Settings") - } - } - - private func getCurrentVersionInfo() -> String { - if let infoDictionary = Bundle.main.infoDictionary { - let version = infoDictionary["CFBundleShortVersionString"] as? String ?? "Unknown" - return "v\(version)" - } - return "Unknown" - } - - private func getCurrentShortcutDisplay() -> String { - let settings = SettingsManager.shared - - if settings.useCustomShortcut && settings.customShortcutModifiers != 0 && settings.customShortcutKey != 0 { - // Use custom shortcut - return formatCurrentShortcut() - } else { - // Use default shortcut - return "โŒ˜โ‡ง4" - } - } - - private func openDeveloperEmail() { - let subject = "ShotScreen \(getCurrentVersionInfo()) - Feedback" - let body = "Hi,\n\nI have a question/feedback about ShotScreen:\n\n" - - // URL encode the subject and body - guard let encodedSubject = subject.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), - let encodedBody = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { - print("โŒ Failed to encode email parameters") - return - } - - let mailtoString = "mailto:info@shotscreen.app?subject=\(encodedSubject)&body=\(encodedBody)" - - if let url = URL(string: mailtoString) { - NSWorkspace.shared.open(url) - print("๐Ÿ“ง Opened default email client to contact developer") - } else { - print("โŒ Failed to create mailto URL") - // Fallback: copy email to clipboard - let pasteboard = NSPasteboard.general - pasteboard.clearContents() - pasteboard.setString("info@shotscreen.app", forType: .string) - print("๐Ÿ“‹ Copied email address to clipboard as fallback") - } - } - - // ๐Ÿ”Š NEW: Test sound function - private func testSound() { - if let sound = NSSound(named: tempScreenshotSoundType.systemSoundName) { - sound.volume = tempScreenshotSoundVolume - sound.play() - } else { - // Fallback to Pop if selected sound doesn't exist - if let fallbackSound = NSSound(named: "Pop") { - fallbackSound.volume = tempScreenshotSoundVolume - fallbackSound.play() - } - } - } - - // ๐Ÿ—‚๏ธ NEW: Cache management functions - private func updateCacheInfo() { - DispatchQueue.global(qos: .background).async { - let size = CacheManager.shared.getCacheSize() - let count = CacheManager.shared.getCacheFileCount() - - DispatchQueue.main.async { - self.cacheSize = size - self.cacheFileCount = count - } - } - } - - private func clearCache() { - DispatchQueue.global(qos: .background).async { - let result = CacheManager.shared.clearCache(preserveActiveThumbnails: true) - - DispatchQueue.main.async { - print("โœ… Cache cleared: \(result.deletedFiles) files, \(String(format: "%.1f", result.savedSpace)) MB freed") - // Update cache info after clearing - self.updateCacheInfo() - } - } - } - - private func openCacheFolder() { - let appSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! - let shotScreenDirectory = appSupportDirectory.appendingPathComponent("ShotScreen") - let thumbnailsDirectory = shotScreenDirectory.appendingPathComponent("Thumbnails") - - // Create directory if it doesn't exist - try? FileManager.default.createDirectory(at: thumbnailsDirectory, withIntermediateDirectories: true, attributes: nil) - - // Open in Finder - NSWorkspace.shared.open(thumbnailsDirectory) - print("๐Ÿ“ Opened cache folder: \(thumbnailsDirectory.path)") - } - - private func openPurchaseURL() { - // ShotScreen Gumroad license product - if let url = URL(string: "https://roodenrijs.gumroad.com/l/uxexr") { - NSWorkspace.shared.open(url) - } - } -} - -class SettingsWindow: NSWindow { - private weak var screenshotDelegate: ScreenshotApp? - private var visualEffectViewContainer: NSView? - - init(currentFolder: URL?, timerValue: Int, delegate: ScreenshotApp) { - self.screenshotDelegate = delegate - - super.init(contentRect: NSMakeRect(0, 0, 600, 800), - styleMask: [.titled, .closable, .resizable, .fullSizeContentView], - backing: .buffered, - defer: false) - - self.title = "ShotScreen Settings" - self.isReleasedWhenClosed = false - self.center() - - // Ensure the window appears in front and can be brought to front - self.level = .normal - self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] - - self.isOpaque = false - self.backgroundColor = .clear - self.titlebarAppearsTransparent = true - self.titleVisibility = .hidden - self.isMovableByWindowBackground = true - - // Hide traffic lights (red, yellow, green buttons) to prevent content overlap - self.standardWindowButton(.closeButton)?.isHidden = true - self.standardWindowButton(.miniaturizeButton)?.isHidden = true - self.standardWindowButton(.zoomButton)?.isHidden = true - - let settingsTabView = SettingsTabView() - let hostingView = NSHostingView(rootView: settingsTabView) - - let visualEffectView = NSVisualEffectView() - visualEffectView.blendingMode = .behindWindow - visualEffectView.material = .hudWindow // krachtigste systeemblur - visualEffectView.state = .active - visualEffectView.alphaValue = 1.0 - visualEffectView.autoresizingMask = [.width, .height] - - // ---------- NIEUW: extra blurlaag ---------- - let extraBlurView = NSVisualEffectView() - extraBlurView.blendingMode = .behindWindow // zelfde type blur - extraBlurView.material = .hudWindow - extraBlurView.state = .active - extraBlurView.alphaValue = 0.6 // half transparant โ†’ optisch meer blur - extraBlurView.autoresizingMask = [.width, .height] - // ------------------------------------------- - - let newRootContentView = NSView(frame: self.contentRect(forFrameRect: self.frame)) - - // volgorde: sterkste blur onderaan, halve blur daarboven, SwiftUI-content erboven - visualEffectView.frame = newRootContentView.bounds - extraBlurView.frame = newRootContentView.bounds - - newRootContentView.addSubview(visualEffectView) // laag 0 - newRootContentView.addSubview(extraBlurView) // laag 1 (versterking) - newRootContentView.addSubview(hostingView) // laag 2 (UI) - - self.contentView = newRootContentView - self.visualEffectViewContainer = newRootContentView - - hostingView.wantsLayer = true - hostingView.layer?.backgroundColor = NSColor.clear.cgColor - - hostingView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - hostingView.topAnchor.constraint(equalTo: newRootContentView.topAnchor), - hostingView.bottomAnchor.constraint(equalTo: newRootContentView.bottomAnchor), - hostingView.leadingAnchor.constraint(equalTo: newRootContentView.leadingAnchor), - hostingView.trailingAnchor.constraint(equalTo: newRootContentView.trailingAnchor) - ]) - - // Forceer een layout pass na het opzetten van de hierarchy - self.contentView?.layoutSubtreeIfNeeded() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc override func close() { - super.close() - } -} - -// Helper om toegang te krijgen tot de ScreenshotApp instantie voor UI updates -// Dit is een beetje een hack; idealiter zou de SettingsManager zelf de updates afhandelen -// of zou er een meer formele delegate/callback structuur zijn. -extension ScreenshotApp { - static var sharedInstance: ScreenshotApp? { - return NSApp.delegate as? ScreenshotApp - } -} - -struct VisualEffectView: NSViewRepresentable { - let material: NSVisualEffectView.Material - let blendingMode: NSVisualEffectView.BlendingMode - - func makeNSView(context: Context) -> NSVisualEffectView { - let view = NSVisualEffectView() - view.material = material - view.blendingMode = blendingMode - view.state = .active - return view - } - - func updateNSView(_ nsView: NSVisualEffectView, context: Context) { - nsView.material = material - nsView.blendingMode = blendingMode - } -} - -// MARK: - Shortcut Recorder Component -struct ShortcutRecorder: NSViewRepresentable { - @Binding var modifiers: UInt - @Binding var keyCode: UInt16 - let placeholder: String - - func makeNSView(context: Context) -> ShortcutRecorderView { - let view = ShortcutRecorderView() - view.placeholder = placeholder - view.onShortcutChange = { modifiers, keyCode in - self.modifiers = modifiers - self.keyCode = keyCode - } - return view - } - - func updateNSView(_ nsView: ShortcutRecorderView, context: Context) { - nsView.modifiers = modifiers - nsView.keyCode = keyCode - nsView.placeholder = placeholder - } -} - -class ShortcutRecorderView: NSView { - var placeholder: String = "Click to record shortcut" { - didSet { needsDisplay = true } - } - var modifiers: UInt = 0 { - didSet { needsDisplay = true } - } - var keyCode: UInt16 = 0 { - didSet { needsDisplay = true } - } - var onShortcutChange: ((UInt, UInt16) -> Void)? - - private var isRecording = false - private var localEventMonitor: Any? - private var globalEventMonitor: Any? - - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - setup() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setup() - } - - private func setup() { - wantsLayer = true - layer?.cornerRadius = 8 - layer?.borderWidth = 2 - layer?.borderColor = NSColor.separatorColor.cgColor - layer?.backgroundColor = NSColor.controlBackgroundColor.cgColor - - // Make this view focusable and always accept first responder - needsDisplay = true - } - - override var acceptsFirstResponder: Bool { return true } - override var canBecomeKeyView: Bool { return true } - - // Force focus when clicked - override func becomeFirstResponder() -> Bool { - let result = super.becomeFirstResponder() - if result { - layer?.borderColor = NSColor.systemBlue.cgColor - needsDisplay = true - } - return result - } - - override func resignFirstResponder() -> Bool { - let result = super.resignFirstResponder() - layer?.borderColor = NSColor.separatorColor.cgColor - needsDisplay = true - return result - } - - override func draw(_ dirtyRect: NSRect) { - super.draw(dirtyRect) - - // Draw background with recording state - if isRecording { - NSColor.controlAccentColor.withAlphaComponent(0.1).setFill() - bounds.fill() - } - - let text: String - if modifiers == 0 && keyCode == 0 { - text = isRecording ? "โŒจ๏ธ Press shortcut keys... (ESC to cancel)" : placeholder - } else { - text = formatShortcut(modifiers: modifiers, keyCode: keyCode) - } - - let fontSize: CGFloat = isRecording ? 12 : 13 - let attrs: [NSAttributedString.Key: Any] = [ - .font: NSFont.systemFont(ofSize: fontSize, weight: isRecording ? .medium : .regular), - .foregroundColor: isRecording ? NSColor.systemBlue : NSColor.controlTextColor - ] - - let attributedString = NSAttributedString(string: text, attributes: attrs) - let textRect = attributedString.boundingRect(with: bounds.size, options: .usesLineFragmentOrigin) - let centeredRect = NSRect( - x: (bounds.width - textRect.width) / 2, - y: (bounds.height - textRect.height) / 2, - width: textRect.width, - height: textRect.height - ) - - attributedString.draw(in: centeredRect) - - // Draw recording indicator - if isRecording { - let dotSize: CGFloat = 6 - let dotRect = NSRect( - x: bounds.width - dotSize - 8, - y: bounds.height - dotSize - 8, - width: dotSize, - height: dotSize - ) - NSColor.systemRed.setFill() - NSBezierPath(ovalIn: dotRect).fill() - } - } - - override func mouseDown(with event: NSEvent) { - // Force focus first, then start recording - if window?.makeFirstResponder(self) == true { - // Small delay to ensure focus is properly established - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - self.startRecording() - } - } else { - startRecording() - } - } - - override func keyDown(with event: NSEvent) { - if isRecording { - let modifierFlags = event.modifierFlags.intersection([.command, .shift, .option, .control]) - let newModifiers = convertModifiers(modifierFlags) - let newKeyCode = event.keyCode - - print("๐ŸŽน ShortcutRecorderView: keyDown - modifiers: \(newModifiers), keyCode: \(newKeyCode)") - - // Require at least one modifier - if newModifiers != 0 { - self.modifiers = newModifiers - self.keyCode = newKeyCode - onShortcutChange?(newModifiers, newKeyCode) - stopRecording() - } else if event.keyCode == 53 { // ESC key - print("๐ŸŽน ShortcutRecorderView: ESC pressed, stopping recording") - stopRecording() - } - } - } - - private func startRecording() { - guard !isRecording else { return } - - print("๐ŸŽน ShortcutRecorderView: Starting recording...") - isRecording = true - needsDisplay = true - - // Force the window to be key and this view to be first responder - window?.makeKey() - window?.makeFirstResponder(self) - - // Add both local and global event monitors for maximum coverage - localEventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.keyDown, .flagsChanged]) { [weak self] event in - guard let self = self, self.isRecording else { return event } - - print("๐ŸŽน Local monitor: Event type: \(event.type.rawValue), keyCode: \(event.keyCode)") - - if event.type == .keyDown { - self.keyDown(with: event) - return nil // Consume the event - } - - return event - } - - // Global monitor as backup to catch events even if focus is lost - globalEventMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.keyDown]) { [weak self] event in - guard let self = self, self.isRecording else { return } - - print("๐ŸŽน Global monitor: keyDown - keyCode: \(event.keyCode)") - - let modifierFlags = event.modifierFlags.intersection([.command, .shift, .option, .control]) - let newModifiers = self.convertModifiers(modifierFlags) - let newKeyCode = event.keyCode - - // Require at least one modifier - if newModifiers != 0 { - DispatchQueue.main.async { - self.modifiers = newModifiers - self.keyCode = newKeyCode - self.onShortcutChange?(newModifiers, newKeyCode) - self.stopRecording() - } - } else if event.keyCode == 53 { // ESC key - DispatchQueue.main.async { - self.stopRecording() - } - } - } - - print("๐ŸŽน ShortcutRecorderView: Recording started, monitors added") - } - - private func stopRecording() { - guard isRecording else { return } - - print("๐ŸŽน ShortcutRecorderView: Stopping recording...") - isRecording = false - needsDisplay = true - - // Remove both event monitors - if let monitor = localEventMonitor { - NSEvent.removeMonitor(monitor) - localEventMonitor = nil - } - - if let monitor = globalEventMonitor { - NSEvent.removeMonitor(monitor) - globalEventMonitor = nil - } - - // Don't immediately resign first responder - let user click elsewhere - print("๐ŸŽน ShortcutRecorderView: Recording stopped, monitors removed") - } - - // Add cleanup when view is removed - deinit { - stopRecording() - print("๐ŸŽน ShortcutRecorderView: deinit") - } - - private func convertModifiers(_ flags: NSEvent.ModifierFlags) -> UInt { - var result: UInt = 0 - if flags.contains(.command) { result |= 1 << 0 } - if flags.contains(.shift) { result |= 1 << 1 } - if flags.contains(.option) { result |= 1 << 2 } - if flags.contains(.control) { result |= 1 << 3 } - return result - } - - private func formatShortcut(modifiers: UInt, keyCode: UInt16) -> String { - var parts: [String] = [] - - if modifiers & (1 << 3) != 0 { parts.append("โŒƒ") } // Control - if modifiers & (1 << 2) != 0 { parts.append("โŒฅ") } // Option - if modifiers & (1 << 1) != 0 { parts.append("โ‡ง") } // Shift - if modifiers & (1 << 0) != 0 { parts.append("โŒ˜") } // Command - - if let keyName = keyCodeToString(keyCode) { - parts.append(keyName) - } - - return parts.joined() - } - - private func keyCodeToString(_ keyCode: UInt16) -> String? { - // Map common key codes to readable strings - switch keyCode { - case 0: return "A" - case 1: return "S" - case 2: return "D" - case 3: return "F" - case 4: return "H" - case 5: return "G" - case 6: return "Z" - case 7: return "X" - case 8: return "C" - case 9: return "V" - case 11: return "B" - case 12: return "Q" - case 13: return "W" - case 14: return "E" - case 15: return "R" - case 16: return "Y" - case 17: return "T" - case 18: return "1" - case 19: return "2" - case 20: return "3" - case 21: return "4" - case 22: return "6" - case 23: return "5" - case 24: return "=" - case 25: return "9" - case 26: return "7" - case 27: return "-" - case 28: return "8" - case 29: return "0" - case 30: return "]" - case 31: return "O" - case 32: return "U" - case 33: return "[" - case 34: return "I" - case 35: return "P" - case 37: return "L" - case 38: return "J" - case 39: return "'" - case 40: return "K" - case 41: return ";" - case 42: return "\\" - case 43: return "," - case 44: return "/" - case 45: return "N" - case 46: return "M" - case 47: return "." - case 50: return "`" - case 36: return "โ†ฉ" // Return - case 48: return "โ‡ฅ" // Tab - case 49: return "Space" - case 51: return "โŒซ" // Delete - case 53: return "โŽ‹" // Escape - case 122: return "F1" - case 120: return "F2" - case 99: return "F3" - case 118: return "F4" - case 96: return "F5" - case 97: return "F6" - case 98: return "F7" - case 100: return "F8" - case 101: return "F9" - case 109: return "F10" - case 103: return "F11" - case 111: return "F12" - default: return "\(keyCode)" - } - } -} - -// MARK: - Login Instructions Popup View -struct LoginInstructionsPopupView: View { - @Environment(\.presentationMode) var presentationMode - let openSettingsAction: () -> Void - - var body: some View { - VStack(spacing: 20) { - // Header - HStack { - Text("How to Add ShotScreen to Login Items") - .font(.title2) - .fontWeight(.bold) - - Spacer() - - Button("Close") { - presentationMode.wrappedValue.dismiss() - } - .buttonStyle(.bordered) - .controlSize(.small) - } - .padding(.top, 20) - .padding(.horizontal, 20) - - // Instructions text (no image needed for login items) - VStack(alignment: .leading, spacing: 12) { - Text("Follow these steps:") - .font(.headline) - .fontWeight(.medium) - .padding(.bottom, 4) - - VStack(alignment: .leading, spacing: 10) { - HStack(alignment: .top, spacing: 8) { - Text("1.") - .font(.body) - .fontWeight(.medium) - .foregroundColor(.blue) - .frame(width: 20) - Text("Click 'Open Settings' below to open System Settings") - .font(.body) - } - - HStack(alignment: .top, spacing: 8) { - Text("2.") - .font(.body) - .fontWeight(.medium) - .foregroundColor(.blue) - .frame(width: 20) - Text("Navigate to General โ†’ Login Items") - .font(.body) - } - - HStack(alignment: .top, spacing: 8) { - Text("3.") - .font(.body) - .fontWeight(.medium) - .foregroundColor(.blue) - .frame(width: 20) - Text("Click the + button to add a new login item") - .font(.body) - } - - HStack(alignment: .top, spacing: 8) { - Text("4.") - .font(.body) - .fontWeight(.medium) - .foregroundColor(.blue) - .frame(width: 20) - Text("Find and select ShotScreen from Applications folder") - .font(.body) - } - - HStack(alignment: .top, spacing: 8) { - Text("5.") - .font(.body) - .fontWeight(.medium) - .foregroundColor(.blue) - .frame(width: 20) - Text("Click 'Add' to enable automatic startup") - .font(.body) - } - } - .padding(.leading, 8) - } - .padding(.horizontal, 24) - .frame(maxWidth: .infinity, alignment: .leading) - - Spacer() - - // Bottom buttons - HStack(spacing: 12) { - Button("Open Settings") { - print("๐Ÿ”ง Opening Login Items Settings from popup...") - openSettingsAction() - // Don't dismiss popup - let user read instructions while configuring - } - .buttonStyle(.borderedProminent) - - Button("Got it!") { - presentationMode.wrappedValue.dismiss() - } - .buttonStyle(.bordered) - } - .padding(.bottom, 20) - } - .frame(width: 500, height: 400) - .background(Color(NSColor.windowBackgroundColor)) - } -} - -// MARK: - License Section -struct LicenseSection: View { - @ObservedObject private var licenseManager = LicenseManager.shared - @State private var showLicenseWindow = false - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("License") - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.primary) - - Spacer() - - Button("Manage License") { - showLicenseDialog() - } - .buttonStyle(.bordered) - } - Divider() - } - - VStack(alignment: .leading, spacing: 12) { - // License Status Display - switch licenseManager.licenseStatus { - case .checking: - HStack { - ProgressView() - .scaleEffect(0.8) - Text("Checking license status...") - .foregroundColor(.secondary) - } - - case .trial(let daysLeft): - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "clock.fill") - .foregroundColor(.orange) - Text("Free Trial Active") - .font(.headline) - .foregroundColor(.orange) - } - - Text("\(daysLeft) days remaining") - .font(.subheadline) - .foregroundColor(.secondary) - - Text("Enjoy your free trial! Purchase a license before it expires to continue using ShotScreen.") - .font(.caption) - .foregroundColor(.secondary) - - HStack(spacing: 12) { - Button("Purchase License") { - openPurchaseURL() - } - .buttonStyle(.borderedProminent) - - Button("Enter License Key") { - showLicenseDialog() - } - .buttonStyle(.bordered) - } - } - - case .licensed(let userName, let email): - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.green) - Text("Licensed") - .font(.headline) - .foregroundColor(.green) - } - - VStack(alignment: .leading, spacing: 4) { - Text("Licensed to: \(userName)") - .font(.subheadline) - .foregroundColor(.primary) - - if !email.isEmpty { - Text("Email: \(email)") - .font(.caption) - .foregroundColor(.secondary) - } - } - - HStack { - Button("Change License") { - showLicenseDialog() - } - .buttonStyle(.bordered) - .controlSize(.small) - } - } - - case .testLicense(let userName, let email): - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "flask.fill") - .foregroundColor(.blue) - Text("Test License") - .font(.headline) - .foregroundColor(.blue) - } - - VStack(alignment: .leading, spacing: 4) { - Text("Test license for: \(userName)") - .font(.subheadline) - .foregroundColor(.primary) - - if !email.isEmpty { - Text("Email: \(email)") - .font(.caption) - .foregroundColor(.secondary) - } - - Text("๐Ÿงช This is a development/test license") - .font(.caption) - .foregroundColor(.blue) - .fontWeight(.medium) - } - - HStack { - Button("Change License") { - showLicenseDialog() - } - .buttonStyle(.bordered) - .controlSize(.small) - } - } - - case .expired: - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(.red) - Text("Trial Expired") - .font(.headline) - .foregroundColor(.red) - } - - Text("Your trial has expired. Please purchase a license to continue using ShotScreen.") - .font(.subheadline) - .foregroundColor(.secondary) - - HStack(spacing: 12) { - Button("Purchase License") { - openPurchaseURL() - } - .buttonStyle(.borderedProminent) - - Button("Enter License Key") { - showLicenseDialog() - } - .buttonStyle(.bordered) - } - } - - case .invalid: - VStack(alignment: .leading, spacing: 8) { - HStack { - Image(systemName: "xmark.circle.fill") - .foregroundColor(.red) - Text("Invalid License") - .font(.headline) - .foregroundColor(.red) - } - - Text("Your license is invalid or has been revoked. Please contact support or purchase a new license.") - .font(.subheadline) - .foregroundColor(.secondary) - - HStack(spacing: 12) { - Button("Purchase License") { - openPurchaseURL() - } - .buttonStyle(.borderedProminent) - - Button("Enter License Key") { - showLicenseDialog() - } - .buttonStyle(.bordered) - } - } - } - - } - } - .padding(20) - .background(Color.primary.opacity(0.03)) - .cornerRadius(12) - .sheet(isPresented: $showLicenseWindow) { - LicenseEntryView() - } - .onReceive(licenseManager.$showLicenseEntry) { shouldShow in - showLicenseWindow = shouldShow - } - } - - private func showLicenseDialog() { - LicenseManager.shared.showLicenseEntryDialog() - } - - private func openPurchaseURL() { - // ShotScreen Gumroad license product - if let url = URL(string: "https://roodenrijs.gumroad.com/l/uxexr") { - NSWorkspace.shared.open(url) - } - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/StashDraggableImageView.swift b/ShotScreen/Sources/StashDraggableImageView.swift deleted file mode 100644 index e501ae9..0000000 --- a/ShotScreen/Sources/StashDraggableImageView.swift +++ /dev/null @@ -1,1140 +0,0 @@ -import AppKit -import SwiftUI -import UniformTypeIdentifiers // NIEUW: Voor UTType support - -// NIEUW: Wrapper class voor PNG data dragging naar browsers -class PNGDataWrapper: NSObject, NSPasteboardWriting { - let data: Data - - init(data: Data) { - self.data = data - } - - func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] { - // Gereduceerd tot pure image data types + generiek public.data - return [ - .png, // Native PNG type - NSPasteboard.PasteboardType("public.png"), // Standaard UTI voor PNG - .tiff, // TIFF fallback - // NSPasteboard.PasteboardType("public.tiff"), // Optioneel: UTI voor TIFF - NSPasteboard.PasteboardType.URL // Voor het plakken als URL in sommige apps (bijv. Notes) - // VERWIJDERD: .fileURL and NSFilenamesPboardType om conflicten te voorkomen - // VERWIJDERD: NSPasteboard.PasteboardType("public.file-url") - ] - } - - func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? { - switch type { - case .png, NSPasteboard.PasteboardType("public.png"): - print("๐ŸŽ PNGDataWrapper: Providing PNG data") - return data - case .tiff: - print("๐ŸŽ PNGDataWrapper: Providing TIFF data (via PNG data)") - return data // PNG data kan vaak als TIFF gelezen worden - case .URL: // Case voor NSPasteboard.PasteboardType.URL - print("๐ŸŽ PNGDataWrapper: Providing data as a base64 data URL string") - let base64String = data.base64EncodedString() - // Maak een data URL. Zorg ervoor dat dit formaat correct is voor de doelapplicaties. - // Dit is een voorbeeld; niet alle apps ondersteunen het plakken van data-URLs. - return "data:image/png;base64,\(base64String)" - default: - print("๐ŸŽ PNGDataWrapper: Type '\(type)' niet ondersteund voor property list") - return nil - } - } -} - -// MARK: - Stash Grid Delegate Protocol -protocol StashGridDelegate: AnyObject { - func stashGridDidDropImage(at cellIndex: Int, stashItem: IdentifiableImage, imageURL: URL) -} - -// MARK: - Stash Grid Manager -class StashGridManager { - var gridWindow: StashGridWindow? - var delegate: StashGridDelegate? - - // GEFIXT: Explicit initialization to prevent state inconsistencies - private var proximityTimer: Timer? - private var isDragSessionActive: Bool = false - private var lastStashFrame: NSRect? - - // NIEUW: Initializer om state correct te resetten - init() { - // CRITICAL: Reset all state to ensure consistent behavior between swift run and .app - self.isDragSessionActive = false - self.lastStashFrame = nil - print("๐Ÿ”ถ STASH DEBUG: StashGridManager initialized with clean state - isDragSessionActive: \(self.isDragSessionActive)") - } - - func showGrid(previewFrame: NSRect?, for stashItem: IdentifiableImage, on originatingScreen: NSScreen) { - print("๐Ÿ”ถ STASH DEBUG: StashGridManager: showGrid START for STASH item \(stashItem.id) on screen \(originatingScreen.localizedName)") - print("๐Ÿ”ถ STASH DEBUG: Delegate is \(self.delegate == nil ? "NIL" : "SET") when showing grid.") - print("๐Ÿ”ถ STASH DEBUG: PreviewFrame: \(String(describing: previewFrame))") - - // NIEUW: Stop proximity timer when grid is shown - proximityTimer?.invalidate() - proximityTimer = nil - - if let appDelegate = NSApp.delegate as? ScreenshotApp, - let imageStore = appDelegate.activeStashImageStore { // Krijg referentie naar de image store - print("๐Ÿ”ถ STASH DEBUG: imageStore.images.count BEFORE appDelegate calls: \(imageStore.images.count)") - print("๐Ÿ”ถ STASH DEBUG: Stopping main grid proximity monitoring to prevent conflicts") - appDelegate.gridViewManager?.stopDragSession() // Kan UI updates triggeren - appDelegate.gridViewManager?.hideGrid() // Kan UI updates triggeren - print("๐Ÿ”ถ STASH DEBUG: imageStore.images.count AFTER appDelegate calls: \(imageStore.images.count)") - } else { - print("๐Ÿ”ถ STASH DEBUG: Kon appDelegate of activeStashImageStore niet verkrijgen voor pre/post logging.") - } - - // Close existing stash grid if any - if let existingWindow = self.gridWindow { - print("๐Ÿ”ถ STASH DEBUG: Closing existing stash grid window") - existingWindow.orderOut(nil) - self.gridWindow = nil - } - - // AANGEPAST: Gebruik stash window frame voor positionering in plaats van preview frame - var effectivePreviewFrame: NSRect - - if let providedFrame = previewFrame { - effectivePreviewFrame = providedFrame - print("๐Ÿ”ถ STASH DEBUG: Using provided preview frame: \(providedFrame)") - } else { - // VERBETERD: Real-time stash window detection met betere logging - var stashWindowFrame: NSRect? - print("๐Ÿ” STASH DEBUG: FALLBACK - Scanning \(NSApp.windows.count) windows for stash window...") - - for (index, window) in NSApp.windows.enumerated() { - let windowTitle = window.title - let hasStashInTitle = windowTitle.contains("Stash") - let hasIntegratedGallery = window.contentView?.className.contains("IntegratedGalleryView") == true - let windowFrame = window.frame - let isVisible = window.isVisible - - print("๐Ÿ” STASH DEBUG: Window[\(index)]: title='\(windowTitle)', hasStash=\(hasStashInTitle), hasGallery=\(hasIntegratedGallery), visible=\(isVisible), frame=\(windowFrame)") - - if (hasStashInTitle || hasIntegratedGallery) && isVisible { - stashWindowFrame = windowFrame - print("๐Ÿ” STASH DEBUG: โœ… FOUND active stash window - frame: \(windowFrame)") - break - } - } - - if let foundFrame = stashWindowFrame { - effectivePreviewFrame = foundFrame - print("๐Ÿ”ถ STASH DEBUG: Using FOUND real-time stash window frame: \(foundFrame)") - } else { - // LAATSTE FALLBACK: Gebruik screen center - let fallbackFrame = NSRect(x: originatingScreen.frame.midX - 100, y: originatingScreen.frame.midY - 50, width: 200, height: 100) - effectivePreviewFrame = fallbackFrame - print("โš ๏ธ STASH DEBUG: NO stash window found - using screen center fallback: \(fallbackFrame)") - } - } - - // NIEUW: Store stash frame for proximity monitoring - self.lastStashFrame = effectivePreviewFrame - - print("๐Ÿ”ถ STASH DEBUG: Using provided screen for stash grid: \(originatingScreen.localizedName)") - print("๐Ÿ”ถ STASH DEBUG: Screen frame: \(originatingScreen.frame)") - print("๐Ÿ”ถ STASH DEBUG: Screen visible frame: \(originatingScreen.visibleFrame)") - - // AANGEPAST: IDENTIEKE POSITIONERING ALS MAIN GRID - ALTIJD RECHTS VAN SCHERM - let gridSize = NSSize(width: 176, height: 344) // 7 cells * 44 + spacing - let spacing: CGFloat = 16 - - // NIEUW: Gebruik ABSOLUTE positionering zoals main grid (GridWindow line 86-89) - var gridOrigin: NSPoint - let screenVisibleFrame = originatingScreen.visibleFrame - - // HOOFDREGEL: ALTIJD RECHTS VAN SCHERM (blijft hetzelfde) - let gridX = screenVisibleFrame.maxX - gridSize.width - 10 // Identiek aan main grid + 10px padding - - // NIEUW: VERTICALE POSITIONERING - BOVEN STASH WINDOW - var gridY: CGFloat - let gridAboveStash = effectivePreviewFrame.maxY + spacing // Boven stash window - - // CHECK 1: Past grid boven stash binnen scherm? - if gridAboveStash + gridSize.height <= screenVisibleFrame.maxY { - gridY = gridAboveStash - print("๐Ÿ”ถ STASH DEBUG: Positioning grid ABOVE stash window") - } - // FALLBACK: Grid onder stash window - else { - gridY = effectivePreviewFrame.minY - gridSize.height - spacing - print("๐Ÿ”ถ STASH DEBUG: Not enough space above - positioning grid BELOW stash window") - - // CHECK 2: Past grid onder stash binnen scherm? - if gridY < screenVisibleFrame.minY { - // Laatste redmiddel: naast stash window (midden) - gridY = effectivePreviewFrame.midY - gridSize.height / 2 - print("๐Ÿ”ถ STASH DEBUG: Not enough space below - positioning grid BESIDE stash window (centered)") - } - } - - gridOrigin = NSPoint(x: gridX, y: gridY) - print("๐Ÿ”ถ STASH DEBUG: Positioning grid at RIGHT edge of screen, smart vertical placement") - print("๐Ÿ”ถ STASH DEBUG: Grid X position: screen.maxX(\(screenVisibleFrame.maxX)) - gridWidth(\(gridSize.width)) - 10px = \(gridX)") - print("๐Ÿ”ถ STASH DEBUG: Grid Y position: \(gridY) (relative to stash at Y=\(effectivePreviewFrame.minY)-\(effectivePreviewFrame.maxY))") - - // BACKUP POSITIONERING: Als er geen effectivePreviewFrame is, centreer alles - if effectivePreviewFrame.width == 0 || effectivePreviewFrame.height == 0 { - gridOrigin.y = screenVisibleFrame.midY - gridSize.height / 2 - print("๐Ÿ”ถ STASH DEBUG: No valid stash window frame, centering grid vertically on screen") - } - - // Bounds checking (behoud de bestaande logica) - EXTRA VEILIGHEID - gridOrigin.x = max(screenVisibleFrame.minX, min(gridOrigin.x, screenVisibleFrame.maxX - gridSize.width)) - gridOrigin.y = max(screenVisibleFrame.minY, min(gridOrigin.y, screenVisibleFrame.maxY - gridSize.height)) - - let gridRect = NSRect(origin: gridOrigin, size: gridSize) - print("๐Ÿ”ถ STASH DEBUG: Final calculated grid rect: \(gridRect)") - - print("๐Ÿ”ถ STASH DEBUG: Creating NEW StashGridWindow (not regular GridWindow)") - let newGridWindow = StashGridWindow(contentRect: gridRect, stashItem: stashItem, manager: self) - - // AANGEPAST: Assign the grid window - self.gridWindow = newGridWindow - - guard let assignedGridWindow = self.gridWindow else { - print("โŒ STASH DEBUG: ERROR - Failed to assign StashGridWindow!") - return - } - - print("๐Ÿ”ถ STASH DEBUG: StashGridWindow created successfully") - print("๐Ÿ”ถ STASH DEBUG: Grid window frame: \(assignedGridWindow.frame)") - print("๐Ÿ”ถ STASH DEBUG: Grid window level: \(assignedGridWindow.level)") - - // Setup window properties - assignedGridWindow.alphaValue = 0 - print("๐Ÿ”ถ STASH DEBUG: Set initial alpha to 0") - - assignedGridWindow.makeKeyAndOrderFront(nil) - print("๐Ÿ”ถ STASH DEBUG: Called makeKeyAndOrderFront") - - assignedGridWindow.orderFrontRegardless() - print("๐Ÿ”ถ STASH DEBUG: Called orderFrontRegardless") - - assignedGridWindow.isInitialFadingIn = true - print("๐Ÿ”ถ STASH DEBUG: Set isInitialFadingIn = true") - - // NIEUW: Make window first responder voor key events (ESC) - _ = assignedGridWindow.becomeFirstResponder() - print("๐Ÿ”ถ STASH DEBUG: Made window first responder for key events") - - print("๐Ÿ”ถ STASH DEBUG: Grid window isVisible: \(assignedGridWindow.isVisible)") - print("๐Ÿ”ถ STASH DEBUG: Grid window alphaValue: \(assignedGridWindow.alphaValue)") - - print("๐Ÿ”ถ STASH DEBUG: Animating stash grid appearance") - NSAnimationContext.runAnimationGroup { ctx in - ctx.duration = 0.2 - assignedGridWindow.animator().alphaValue = 1 - print("๐Ÿ”ถ STASH DEBUG: Animation started - target alpha: 1.0") - } completionHandler: { - assignedGridWindow.isInitialFadingIn = false - print("๐Ÿ”ถ STASH DEBUG: Stash grid appearance animation complete") - print("๐Ÿ”ถ STASH DEBUG: Final grid window alphaValue: \(assignedGridWindow.alphaValue)") - print("๐Ÿ”ถ STASH DEBUG: Final grid window isVisible: \(assignedGridWindow.isVisible)") - } - } - - func hideGrid() { - print("๐Ÿ”ถ STASH DEBUG: StashGridManager: hideGrid called for STASH") - guard let window = gridWindow else { - print("๐Ÿ”ถ STASH DEBUG: No stash grid window to hide") - return - } - - // NIEUW: Stop proximity timer when manually hiding - proximityTimer?.invalidate() - proximityTimer = nil - - print("๐Ÿ”ถ STASH DEBUG: Hiding stash grid window") - NSAnimationContext.runAnimationGroup({ ctx in - ctx.duration = 0.2 - window.animator().alphaValue = 0 - }, completionHandler: { [weak self] in - print("๐Ÿ”ถ STASH DEBUG: Stash grid hide animation complete") - window.orderOut(nil) - if self?.gridWindow === window { - self?.gridWindow = nil - } - }) - } - - // NIEUW: Drag session management (identiek aan main grid) - func startDragSession() { - print("๐Ÿ”ถ STASH DEBUG: Drag session started - enabling proximity monitoring") - isDragSessionActive = true - } - - func stopDragSession() { - print("๐Ÿ”ถ STASH DEBUG: Drag session ended - disabling proximity monitoring") - isDragSessionActive = false - - // GEFIXT: Alleen start proximity monitoring als er nog een actieve drag zou zijn - // Dit voorkomt dat de grid blijft hangen na een drag - // (Main grid heeft dezelfde logica) - print("๐Ÿ”ถ STASH DEBUG: Drag session stopped - grid should hide soon unless mouse stays near") - - // NIEUW: Start een eenvoudige timer om de grid te verbergen na een korte delay - // Dit geeft de gebruiker de kans om nog te interacteren, maar zorgt dat de grid verdwijnt - if gridWindow != nil { - print("๐Ÿ”ถ STASH DEBUG: Starting auto-hide timer for stash grid") - DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in - // Alleen verbergen als er geen actieve drag is - if self?.isDragSessionActive == false { - print("๐Ÿ”ถ STASH DEBUG: Auto-hiding stash grid after drag completion") - self?.hideGrid() - } - } - } - } - - // GEFIXT: Proximity monitoring alleen starten wanneer expliciet gevraagd - private func startProximityMonitoring() { - // KRITIEK: Check of er een actieve drag sessie is - // Anders start de monitoring niet (consistent met main grid) - guard isDragSessionActive else { - print("๐Ÿ”ถ STASH DEBUG: Skipping proximity monitoring - no active drag session") - return - } - - guard let targetFrame = lastStashFrame else { - print("๐Ÿ”ถ STASH DEBUG: No stash frame - skipping proximity monitoring") - return - } - - // Stop existing timer - proximityTimer?.invalidate() - - print("๐Ÿ”ถ STASH DEBUG: Starting proximity monitoring (only during active drag)") - proximityTimer = Timer.scheduledTimer(withTimeInterval: 0.12, repeats: true) { [weak self] _ in - self?.evaluateMouseProximity(to: targetFrame) - } - RunLoop.main.add(proximityTimer!, forMode: .common) - } - - private func evaluateMouseProximity(to frame: NSRect) { - // GEFIXT: Check of er een actieve drag sessie is - guard isDragSessionActive else { - print("๐Ÿ”ถ STASH DEBUG: Stopping proximity evaluation - no active drag session") - proximityTimer?.invalidate() - proximityTimer = nil - // NIEUW: Hide grid when no active drag session - hideGrid() - return - } - - // VERBETERD: Gebruik real-time stash window frame in plaats van cached frame - var currentStashFrame = frame // Start met cached frame - - // NIEUW: Probeer real-time stash window frame te verkrijgen - for window in NSApp.windows { - let windowTitle = window.title - if (windowTitle.contains("Stash") || window.contentView?.className.contains("IntegratedGalleryView") == true) && window.isVisible { - currentStashFrame = window.frame - // print("๐Ÿ”ถ PROXIMITY DEBUG: Using real-time stash frame: \(currentStashFrame)") - break - } - } - - // Bereken een vergrote zone (200 px marge) rondom de REAL-TIME stash window - let expansion: CGFloat = 200 - let enlarged = currentStashFrame.insetBy(dx: -expansion, dy: -expansion) - - let currentLoc = NSEvent.mouseLocation - if !enlarged.contains(currentLoc) { - // Mouse is ver weg - verberg grid - print("๐Ÿ”ถ STASH DEBUG: Mouse far from stash window - hiding grid") - proximityTimer?.invalidate() - proximityTimer = nil - hideGrid() - } - // Als mouse nog in de buurt is, blijft grid zichtbaar - } -} - -// MARK: - Stash Grid Window -class StashGridWindow: NSWindow, NSDraggingDestination { - var cellViews: [GridCellView] = [] - weak var manager: StashGridManager? - private var currentlyHighlightedCell: GridCellView? - private var fadeTimer: Timer? - private let fadeStart: CGFloat = 50 - private let fadeEnd: CGFloat = 300 - private let minAlpha: CGFloat = 0 - var isInitialFadingIn: Bool = false - var isPerformingProgrammaticHide: Bool = false - - // Store stash item reference for actions - let stashItem: IdentifiableImage - - init(contentRect: NSRect, stashItem: IdentifiableImage, manager: StashGridManager) { - self.manager = manager - self.stashItem = stashItem - let settings = SettingsManager.shared - - // IDENTIEK AAN MAIN GRID: Gebruik dezelfde settings en layout - var activeActions: [(index: Int, text: String)] = [] - - for (gridIndex, actionType) in settings.actionOrder.enumerated() { - let isEnabled: Bool - let displayText: String - - switch actionType { - case .rename: - isEnabled = settings.isRenameActionEnabled - displayText = "Rename" - case .stash: - isEnabled = settings.isStashActionEnabled - displayText = "Stash" - case .ocr: - isEnabled = settings.isOCRActionEnabled - displayText = "Text" - case .clipboard: - isEnabled = settings.isClipboardActionEnabled - displayText = "Clipboard" - case .backgroundRemove: - isEnabled = settings.isBackgroundRemoveActionEnabled - displayText = "Remove BG" - case .cancel: - isEnabled = settings.isCancelActionEnabled - displayText = "Cancel" - case .remove: - isEnabled = settings.isRemoveActionEnabled - displayText = "Remove" - } - - if isEnabled { - activeActions.append((gridIndex, displayText)) - } - } - - let numberOfActiveActions = activeActions.count - guard numberOfActiveActions > 0 else { - print("๐Ÿ”ท STASH DEBUG: No active actions - creating empty stash grid window") - super.init(contentRect: .zero, styleMask: .borderless, backing: .buffered, defer: false) - self.isOpaque = false - self.backgroundColor = .clear - self.ignoresMouseEvents = true - DispatchQueue.main.async { manager.hideGrid() } - return - } - - print("๐Ÿ”ท STASH DEBUG: StashGridWindow init: Number of active actions = \(numberOfActiveActions)") - print("๐Ÿ”ท STASH DEBUG: Using same layout as main grid but for STASH actions") - - // IDENTIEKE LAYOUT ALS MAIN GRID - let cellsPerRow = 1 - let numberOfRows = Int(ceil(Double(numberOfActiveActions) / Double(cellsPerRow))) - let spacing: CGFloat = 8 - let fixedCellHeight: CGFloat = 40.0 - let fixedCellWidth: CGFloat = 160.0 - - let calculatedGridWidth = (fixedCellWidth * CGFloat(cellsPerRow)) + (spacing * (CGFloat(cellsPerRow) + 1)) - let calculatedGridHeight = (fixedCellHeight * CGFloat(numberOfRows)) + (spacing * (CGFloat(numberOfRows) + 1)) - - var xPosition: CGFloat - var yPosition: CGFloat - - xPosition = contentRect.origin.x - yPosition = contentRect.origin.y - - let contentRect = NSRect(x: xPosition, y: yPosition, width: calculatedGridWidth, height: calculatedGridHeight) - - print("๐Ÿ”ท STASH DEBUG: StashGridWindow init: Calculated contentRect = \(contentRect)") - - super.init(contentRect: contentRect, styleMask: [.borderless], backing: .buffered, defer: false) - self.level = .floating + 10 - print("๐Ÿ”ท STASH DEBUG: StashGridWindow init: Window level set to \(self.level.rawValue)") - self.isOpaque = false - self.backgroundColor = .clear - self.hasShadow = false - self.ignoresMouseEvents = false - self.acceptsMouseMovedEvents = true - - // IDENTIEKE STYLING ALS MAIN GRID - let containerView = NSView(frame: NSRect(origin: .zero, size: contentRect.size)) - containerView.wantsLayer = true - - let barBlur1 = NSVisualEffectView() - barBlur1.blendingMode = .behindWindow - barBlur1.material = .hudWindow - barBlur1.state = .active - barBlur1.frame = containerView.bounds - barBlur1.autoresizingMask = [.width, .height] - - let barBlur2 = NSVisualEffectView() - barBlur2.blendingMode = .behindWindow - barBlur2.material = .hudWindow - barBlur2.state = .active - barBlur2.alphaValue = 0.6 - barBlur2.frame = containerView.bounds - barBlur2.autoresizingMask = [.width, .height] - - containerView.addSubview(barBlur1, positioned: .below, relativeTo: nil) - containerView.addSubview(barBlur2, positioned: .below, relativeTo: nil) - - containerView.layer?.cornerRadius = 12 - containerView.layer?.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] - containerView.layer?.masksToBounds = true - self.contentView = containerView - - // Maak cellen - for (gridIndex, action) in activeActions.enumerated() { - let col = gridIndex % cellsPerRow - let row = gridIndex / cellsPerRow - - let cellX = spacing + CGFloat(col) * (fixedCellWidth + spacing) - let cellY = calculatedGridHeight - spacing - CGFloat(row + 1) * fixedCellHeight - CGFloat(row) * spacing - - let cellFrame = NSRect(x: cellX, y: cellY, width: fixedCellWidth, height: fixedCellHeight) - let cellView = GridCellView(frame: cellFrame, index: action.index, text: action.text) - containerView.addSubview(cellView) - cellViews.append(cellView) - } - - print("๐Ÿ”ท STASH DEBUG: StashGridWindow init: Number of cellViews created = \(cellViews.count)") - - registerForDraggedTypes([.fileURL]) - - // Start timer voor dynamische transparantie - fadeTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true, block: { [weak self] _ in - self?.updateAlphaBasedOnCursor() - }) - - print("๐Ÿ”ท STASH DEBUG: StashGridWindow init completed successfully") - } - - deinit { - fadeTimer?.invalidate() - } - - private func updateAlphaBasedOnCursor() { - guard !isPerformingProgrammaticHide else { return } - guard !isInitialFadingIn else { return } - guard let screenPoint = NSEvent.mouseLocation as NSPoint? else { return } - let windowFrame = self.frame - - let distance: CGFloat - if windowFrame.contains(screenPoint) { - distance = 0 - } else { - let dx: CGFloat - if screenPoint.x < windowFrame.minX { dx = windowFrame.minX - screenPoint.x } - else if screenPoint.x > windowFrame.maxX { dx = screenPoint.x - windowFrame.maxX } else { dx = 0 } - - let dy: CGFloat - if screenPoint.y < windowFrame.minY { dy = windowFrame.minY - screenPoint.y } - else if screenPoint.y > windowFrame.maxY { dy = screenPoint.y - windowFrame.maxY } else { dy = 0 } - - distance = sqrt(dx*dx + dy*dy) - } - - let newAlpha: CGFloat - if distance <= fadeStart { newAlpha = 1 } - else if distance >= fadeEnd { newAlpha = minAlpha } - else { - let ratio = (distance - fadeStart) / (fadeEnd - fadeStart) - newAlpha = 1 - ratio * (1 - minAlpha) - } - - if abs(self.alphaValue - newAlpha) > 0.01 { - self.alphaValue = newAlpha - } - } - - required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - // MARK: - NSDraggingDestination Methods - func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation { - return .copy - } - - func draggingUpdated(_ sender: NSDraggingInfo) -> NSDragOperation { - let dropLocationInScreen = sender.draggingLocation - guard let dropLocationInContent = self.contentView?.convert(dropLocationInScreen, from: nil) else { - currentlyHighlightedCell?.setHighlighted(false) - currentlyHighlightedCell = nil - return [] - } - - var foundCell: GridCellView? = nil - for cell in cellViews { - if cell.frame.contains(dropLocationInContent) { - foundCell = cell - break - } - } - - if currentlyHighlightedCell !== foundCell { - currentlyHighlightedCell?.setHighlighted(false) - currentlyHighlightedCell?.setHovered(false) - foundCell?.setHighlighted(true) - foundCell?.setHovered(true) - currentlyHighlightedCell = foundCell - } - - return .copy - } - - func performDragOperation(_ sender: NSDraggingInfo) -> Bool { - guard let pasteboard = sender.draggingPasteboard.propertyList(forType: NSPasteboard.PasteboardType(rawValue: "NSFilenamesPboardType")) as? NSArray, - let path = pasteboard[0] as? String else { - self.manager?.hideGrid() - return false - } - let imageURL = URL(fileURLWithPath: path) - let dropLocationInContent = self.contentView?.convert(sender.draggingLocation, from: nil) ?? .zero - - currentlyHighlightedCell?.setHighlighted(false) - currentlyHighlightedCell = nil - - for cell in cellViews { - if cell.frame.contains(dropLocationInContent) { - if let currentManager = self.manager, let currentActionDelegate = currentManager.delegate { - print("โœ… StashGridWindow: Detected drop on cell \(cell.index). Calling manager's delegate...") - currentActionDelegate.stashGridDidDropImage(at: cell.index, stashItem: stashItem, imageURL: imageURL) - return true - } else { - print("โŒ StashGridWindow: Manager (\(self.manager == nil ? "NIL" : "SET")) or manager.delegate (\(self.manager?.delegate == nil ? "NIL" : "SET")) is nil for drop on cell \(cell.index)!") - if self.manager == nil { - print("โŒ Detail: StashGridWindow.manager is nil.") - } else if self.manager?.delegate == nil { - print("โŒ Detail: StashGridWindow.manager.delegate is nil.") - } - } - self.manager?.hideGrid() - return false - } - } - - self.manager?.hideGrid() - return false - } - - func draggingExited(_ sender: NSDraggingInfo?) { - currentlyHighlightedCell?.setHighlighted(false) - currentlyHighlightedCell?.setHovered(false) - currentlyHighlightedCell = nil - } - - override func mouseUp(with event: NSEvent) { - super.mouseUp(with: event) - - // NIEUW: Hide grid on mouse up outside cells (same behavior as main grid) - let locationInWindow = event.locationInWindow - print("๐Ÿ”ถ STASH DEBUG: StashGridWindow: mouseUp at \(locationInWindow)") - - // Check if click was inside any cell - var clickWasInCell = false - for cellView in cellViews { - let cellFrame = cellView.frame - if cellFrame.contains(locationInWindow) { - clickWasInCell = true - print("๐Ÿ”ถ STASH DEBUG: Click was inside cell at frame \(cellFrame)") - break - } - } - - if !clickWasInCell { - print("๐Ÿ”ถ STASH DEBUG: Click was OUTSIDE all cells - hiding stash grid") - // Close the grid when clicking outside cells - DispatchQueue.main.async { - if let manager = self.manager { - manager.hideGrid() - } else { - // Fallback: hide window directly - self.orderOut(nil) - } - } - } else { - print("๐Ÿ”ถ STASH DEBUG: Click was inside cell - keeping grid open") - } - } - - override func keyDown(with event: NSEvent) { - // NIEUW: Handle ESC key to close stash grid - if event.keyCode == 53 { // ESC key - print("๐Ÿ”ถ STASH DEBUG: ESC key pressed - hiding stash grid") - DispatchQueue.main.async { - if let manager = self.manager { - manager.hideGrid() - } else { - // Fallback: hide window directly - self.orderOut(nil) - } - } - } else { - super.keyDown(with: event) - } - } - - override var canBecomeKey: Bool { - return true // NIEUW: Nodig om key events te ontvangen - } - - override var acceptsFirstResponder: Bool { - return true // NIEUW: Nodig om key events te ontvangen - } -} - -// MARK: - Stash Draggable Image View Protocol -protocol StashDraggableImageViewDelegate: AnyObject { - func stashImageDidStartDrag(imageURL: URL, from view: StashDraggableNSImageView) - func stashImageDragDidEnd(imageURL: URL, operation: NSDragOperation, from view: StashDraggableNSImageView) -} - -// MARK: - Stash Draggable Image View (NSView) -class StashDraggableNSImageView: NSImageView, NSFilePromiseProviderDelegate { - weak var delegate: StashDraggableImageViewDelegate? - var imageURL: URL? - var suggestedName: String? - var stashItem: IdentifiableImage? - private var mouseDownEvent: NSEvent? - private let dragThreshold: CGFloat = 3.0 - private var isPerformingDrag: Bool = false - - // NIEUW: Eigen grid manager voor stash items - var stashGridManager: StashGridManager? - - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - setupView() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - setupView() - } - - private func setupView() { - self.imageScaling = .scaleProportionallyUpOrDown - self.imageAlignment = .alignCenter - self.animates = true - self.imageFrameStyle = .none - self.registerForDraggedTypes([.fileURL, .URL, .tiff, .png]) - - // Maak eigen grid manager aan - self.stashGridManager = StashGridManager() - } - - // NIEUW: Setup delegate wanneer deze wordt geset - func setDelegate(_ delegate: StashDraggableImageViewDelegate) { - self.delegate = delegate - - // VERBETERDE: Robuustere setup van grid manager connectie - if let stashDelegate = delegate as? StashDragDelegate { - // Zorg ervoor dat grid manager bestaat - if self.stashGridManager == nil { - self.stashGridManager = StashGridManager() - print("๐Ÿ”ถ STASH DEBUG: Created new StashGridManager in setDelegate") - } - - // Verbind delegate met grid manager - stashDelegate.setStashGridManager(self.stashGridManager) - } - } - - override func mouseDown(with event: NSEvent) { - self.mouseDownEvent = event - self.isPerformingDrag = false - print("๐Ÿ–ฑ๏ธ STASH DEBUG: StashDraggableNSImageView: mouseDown. Event stored.") - print("๐Ÿ–ฑ๏ธ STASH DEBUG: Mouse location in window: \(event.locationInWindow)") - print("๐Ÿ–ฑ๏ธ STASH DEBUG: imageURL: \(imageURL?.lastPathComponent ?? "nil")") - print("๐Ÿ–ฑ๏ธ STASH DEBUG: stashItem: \(stashItem?.id.uuidString.prefix(8) ?? "nil")") - print("๐Ÿ–ฑ๏ธ STASH DEBUG: stashGridManager: \(stashGridManager != nil ? "SET" : "NIL")") - } - - override func mouseDragged(with event: NSEvent) { - guard let mouseDownEvent = self.mouseDownEvent else { - print("๐Ÿ–ฑ๏ธ STASH DEBUG: mouseDragged called but no mouseDownEvent - calling super") - super.mouseDragged(with: event) - return - } - - print("๐Ÿ–ฑ๏ธ STASH DEBUG: mouseDragged called, isPerformingDrag: \(isPerformingDrag)") - - if !isPerformingDrag { - let deltaX = abs(event.locationInWindow.x - mouseDownEvent.locationInWindow.x) - let deltaY = abs(event.locationInWindow.y - mouseDownEvent.locationInWindow.y) - - print("๐Ÿ–ฑ๏ธ STASH DEBUG: Delta X: \(deltaX), Delta Y: \(deltaY), threshold: \(dragThreshold)") - - if deltaX > dragThreshold || deltaY > dragThreshold { - print("๐Ÿ–ฑ๏ธ STASH DEBUG: โœ… THRESHOLD EXCEEDED - Starting drag process") - isPerformingDrag = true - self.mouseDownEvent = nil - - guard let url = imageURL, let item = stashItem else { - print("โŒ STASH DEBUG: StashDraggableNSImageView: mouseDragged - Missing imageURL or stashItem for drag.") - print("โŒ STASH DEBUG: imageURL is nil: \(imageURL == nil), stashItem is nil: \(stashItem == nil)") - isPerformingDrag = false - return - } - - print("๐ŸŽฏ STASH DEBUG: StashDraggableNSImageView: Starting STASH GRID (not main grid) for item \(item.id)") - - // VERBETERDE: Extra safety check voor grid manager - if self.stashGridManager == nil { - print("โš ๏ธ STASH DEBUG: Grid manager was nil, creating new one during drag") - self.stashGridManager = StashGridManager() - - // Reconnect delegate if available - if let stashDelegate = delegate as? StashDragDelegate { - stashDelegate.setStashGridManager(self.stashGridManager) - print("๐Ÿ”ถ STASH DEBUG: Re-connected delegate during drag") - } - } else { - print("โœ… STASH DEBUG: Grid manager already exists") - } - - // Start stash grid op de juiste screen - if let window = self.window, let screen = window.screen { - print("๐Ÿ–ฑ๏ธ STASH DEBUG: Window and screen available - \(screen.localizedName)") - print("๐Ÿ”ฅ STASH DEBUG: REAL-TIME stash window frame: \(window.frame)") - if let manager = stashGridManager { - print("๐Ÿ–ฑ๏ธ STASH DEBUG: About to call manager.showGrid with REAL-TIME frame...") - // CRITICAL FIX: Pass REAL-TIME stash window frame instead of nil - manager.showGrid(previewFrame: window.frame, for: item, on: screen) - print("๐Ÿ–ฑ๏ธ STASH DEBUG: manager.showGrid called successfully with dynamic frame") - - // NIEUW: Start drag session voor proximity monitoring - manager.startDragSession() - print("๐Ÿ–ฑ๏ธ STASH DEBUG: Started stash drag session for proximity monitoring") - } else { - print("โŒ STASH DEBUG: ERROR - stashGridManager is still nil after safety check!") - isPerformingDrag = false - return - } - } else { - print("โŒ STASH DEBUG: ERROR - No window or screen available!") - print("โŒ STASH DEBUG: self.window is nil: \(self.window == nil)") - if let w = self.window { - print("โŒ STASH DEBUG: window.screen is nil: \(w.screen == nil)") - } - isPerformingDrag = false - return - } - - // Notify delegate about drag start - print("๐Ÿ–ฑ๏ธ STASH DEBUG: Notifying delegate about drag start") - delegate?.stashImageDidStartDrag(imageURL: url, from: self) - - // NIEUW: Voeg ook direct NSImage support toe (zoals hoofdthumbnail) - var draggingItems: [NSDraggingItem] = [] - - // CRITICAL FIX: Use only ONE multifunctional dragging item to prevent count badge - if let stashFileURL = self.stashItem?.fileURL, let nsImage = self.image { - print("๐ŸŽฏ STASH DEBUG: Creating SINGLE multifunctional dragging item WITH NSFilePromiseProvider") - - // ๐Ÿ”ฅ ULTRA FIX: Add thumbnail scaling like hoofdgrid - let fullFrame = convert(bounds, to: nil) - let scale: CGFloat = 0.05 // 5% van originele grootte (exact zoals hoofdgrid) - let yOffset: CGFloat = 30 - let scaledFrame = NSRect( - x: fullFrame.midX - fullFrame.width * scale / 2, - y: fullFrame.midY - fullFrame.height * scale / 2 - yOffset, - width: fullFrame.width * scale, - height: fullFrame.height * scale - ) - - // ULTRA FIX: Add NSFilePromiseProvider for proper filename handling in Finder - let filePromiseProvider = NSFilePromiseProvider(fileType: UTType.png.identifier, delegate: self) - filePromiseProvider.userInfo = [ - "isFilePromise": true, - "fileURL": stashFileURL - ] - - // Create dragging item with file promise provider for Finder compatibility - let filePromiseDragItem = NSDraggingItem(pasteboardWriter: filePromiseProvider) - filePromiseDragItem.setDraggingFrame(scaledFrame, contents: nsImage) // ๐Ÿ”ฅ SCALED! - draggingItems.append(filePromiseDragItem) - - // Create one comprehensive pasteboard item with ALL types for other apps - let comprehensiveItem = NSPasteboardItem() - - // 1. File URL types - comprehensiveItem.setString(stashFileURL.absoluteString, forType: .fileURL) - comprehensiveItem.setString(stashFileURL.absoluteString, forType: NSPasteboard.PasteboardType("public.file-url")) - comprehensiveItem.setString(stashFileURL.path, forType: .string) - - // 2. File path list - if let pathData = try? PropertyListSerialization.data(fromPropertyList: [stashFileURL.path], format: .binary, options: 0) { - comprehensiveItem.setData(pathData, forType: NSPasteboard.PasteboardType("NSFilenamesPboardType")) - } - - // 3. Image data types - if let tiffRepresentation = nsImage.tiffRepresentation, - let bitmapImageRep = NSBitmapImageRep(data: tiffRepresentation), - let pngData = bitmapImageRep.representation(using: .png, properties: [:]) { - comprehensiveItem.setData(pngData, forType: .png) - comprehensiveItem.setData(pngData, forType: NSPasteboard.PasteboardType("public.png")) - comprehensiveItem.setData(tiffRepresentation, forType: .tiff) - - // Base64 data URL for browser compatibility - let base64String = pngData.base64EncodedString() - comprehensiveItem.setString("data:image/png;base64,\(base64String)", forType: .URL) - } - - // Create additional dragging item for compatibility - let compatibilityDragItem = NSDraggingItem(pasteboardWriter: comprehensiveItem) - compatibilityDragItem.setDraggingFrame(scaledFrame, contents: nsImage) // ๐Ÿ”ฅ SCALED! - draggingItems.append(compatibilityDragItem) - - print("๐ŸŽฏ STASH DEBUG: Created comprehensive dragging session with NSFilePromiseProvider + compatibility item") - } else { - // Fallback to simple NSImage - if let nsImage = self.image { - // ๐Ÿ”ฅ ULTRA FIX: Add scaling for fallback too - let fullFrame = convert(bounds, to: nil) - let scale: CGFloat = 0.05 - let yOffset: CGFloat = 30 - let scaledFrame = NSRect( - x: fullFrame.midX - fullFrame.width * scale / 2, - y: fullFrame.midY - fullFrame.height * scale / 2 - yOffset, - width: fullFrame.width * scale, - height: fullFrame.height * scale - ) - - let nsImageDragItem = NSDraggingItem(pasteboardWriter: nsImage) - nsImageDragItem.setDraggingFrame(scaledFrame, contents: nsImage) // ๐Ÿ”ฅ SCALED! - draggingItems.append(nsImageDragItem) - print("๐ŸŽฏ STASH DEBUG: Created fallback NSImage dragging item WITH SCALING") - } - } - - print("๐ŸŽฏ STASH DEBUG: Total dragging items: \(draggingItems.count) (should be 1 - no badge)") - - // FIXED: Veilige access naar mouseDownEvent met fallback - guard let mouseEvent = self.mouseDownEvent else { - print("โŒ STASH DEBUG: mouseDownEvent is nil! Creating fallback event") - // Maak een fallback event met huidige mouse locatie - let currentLocation = NSEvent.mouseLocation - let windowLocation = self.window?.convertPoint(fromScreen: currentLocation) ?? NSPoint.zero - let localLocation = self.convert(windowLocation, from: nil) - - // Create a minimal mouse event as fallback - let fallbackEvent = NSEvent.mouseEvent( - with: .leftMouseDown, - location: localLocation, - modifierFlags: [], - timestamp: ProcessInfo.processInfo.systemUptime, - windowNumber: self.window?.windowNumber ?? 0, - context: nil, - eventNumber: 0, - clickCount: 1, - pressure: 1.0 - ) - - if let fallbackEvent = fallbackEvent { - let draggingSession = self.beginDraggingSession(with: draggingItems, event: fallbackEvent, source: self) - draggingSession.draggingFormation = .none - draggingSession.animatesToStartingPositionsOnCancelOrFail = true - print("๐ŸŽฏ STASH DEBUG: Used fallback event for dragging session") - } else { - print("โŒ STASH DEBUG: Failed to create fallback event - aborting drag") - return - } - return - } - - let draggingSession = self.beginDraggingSession(with: draggingItems, event: mouseEvent, source: self) - draggingSession.draggingFormation = .none - draggingSession.animatesToStartingPositionsOnCancelOrFail = true - - print("๐ŸŽฏ STASH DEBUG: Stash drag session beginning at (\(NSEvent.mouseLocation.x), \(NSEvent.mouseLocation.y))") - print("๐ŸŽฏ STASH DEBUG: Dragging session configured with \(draggingItems.count) items treated as single unit") - } else { - print("๐Ÿ–ฑ๏ธ STASH DEBUG: โธ๏ธ Drag threshold not exceeded yet") - } - } else { - print("๐Ÿ–ฑ๏ธ STASH DEBUG: Already performing drag, ignoring mouseDragged") - } - } - - override func mouseUp(with event: NSEvent) { - print("๐Ÿ–ฑ๏ธ STASH DEBUG: StashDraggableNSImageView: mouseUp called, isPerformingDrag: \(isPerformingDrag)") - - // VERBETERD: Alleen mouseDownEvent clearen als we niet aan het draggen zijn - if !isPerformingDrag { - self.mouseDownEvent = nil - print("๐Ÿ–ฑ๏ธ STASH DEBUG: Cleared mouseDownEvent (not dragging)") - } else { - print("๐Ÿ–ฑ๏ธ STASH DEBUG: Keeping mouseDownEvent during drag operation") - } - } - - // MARK: - NSFilePromiseProviderDelegate - func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String { - // Check if this is a file URL promise - if let userInfo = filePromiseProvider.userInfo as? [String: Any], - let isFilePromise = userInfo["isFilePromise"] as? Bool, - isFilePromise, - let fileURL = userInfo["fileURL"] as? URL { - print("๐Ÿ”„ filePromiseProvider (file URL) asking for filename: '\(fileURL.lastPathComponent)'") - return fileURL.lastPathComponent - } - - // Fallback to original implementation for regular promises - guard let item = stashItem else { - return "StashImage.png" - } - - // Bepaal de originele naam: customName > filename (zonder extensie) > fallback - let originalName: String - if let customName = item.customName, !customName.isEmpty { - originalName = customName - } else if let fileURL = item.fileURL { - originalName = fileURL.deletingPathExtension().lastPathComponent - } else { - originalName = "StashImage" - } - - let filename = "\(originalName).png" - print("๐Ÿ”„ filePromiseProvider asking for filename: '\(filename)'") - return filename - } - - func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: @escaping (Error?) -> Void) { - print("๐Ÿ”„ filePromiseProvider writePromiseTo: \(url.lastPathComponent)") - - // Check if this is a file URL promise - if let userInfo = filePromiseProvider.userInfo as? [String: Any], - let isFilePromise = userInfo["isFilePromise"] as? Bool, - isFilePromise, - let sourceFileURL = userInfo["fileURL"] as? URL { - print("๐Ÿ”„ filePromiseProvider (file URL) copying from: \(sourceFileURL.lastPathComponent)") - - do { - // Check if destination already exists - if FileManager.default.fileExists(atPath: url.path) { - try FileManager.default.removeItem(at: url) - } - - try FileManager.default.copyItem(at: sourceFileURL, to: url) - print("โœ… Successfully copied file URL promise to: \(url.lastPathComponent)") - completionHandler(nil) - } catch { - print("โŒ Failed to copy file URL promise: \(error)") - completionHandler(error) - } - return - } - - // Fallback to original implementation - guard let item = stashItem else { - completionHandler(NSError(domain: "StashDragError", code: 1, userInfo: [NSLocalizedDescriptionKey: "No stash item available"])) - return - } - - // Kopieer het bestand van stash naar de gevraagde locatie - if let stashFileURL = item.fileURL { - do { - // Check if destination already exists - if FileManager.default.fileExists(atPath: url.path) { - try FileManager.default.removeItem(at: url) - } - - try FileManager.default.copyItem(at: stashFileURL, to: url) - print("โœ… Successfully copied stash file to: \(url.lastPathComponent)") - completionHandler(nil) - } catch { - print("โŒ Failed to copy stash file: \(error)") - completionHandler(error) - } - } else { - // Fallback: converteer NSImage naar PNG data - guard let tiffRepresentation = item.nsImage.tiffRepresentation, - let bitmapImageRep = NSBitmapImageRep(data: tiffRepresentation), - let pngData = bitmapImageRep.representation(using: .png, properties: [:]) else { - print("โŒ Could not convert stash image to PNG data") - completionHandler(NSError(domain: "StashDragError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Could not convert image to PNG"])) - return - } - - do { - try pngData.write(to: url) - print("โœ… Successfully wrote stash image data to: \(url.lastPathComponent)") - completionHandler(nil) - } catch { - print("โŒ Failed to write stash image data: \(error)") - completionHandler(error) - } - } - } -} - -// MARK: - NSDraggingSource Extension -extension StashDraggableNSImageView: NSDraggingSource { - func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation { - return .copy - } - - func draggingSession(_ session: NSDraggingSession, willBeginAt screenPoint: NSPoint) { - print("๐ŸŽฏ STASH DEBUG: Stash drag session beginning at \(screenPoint)") - print("๐ŸŽฏ STASH DEBUG: This should ONLY show STASH grid, NEVER main grid") - } - - func draggingSession(_ session: NSDraggingSession, movedTo screenPoint: NSPoint) { - // Stash items hebben geen proximity feedback - simpel houden - // print("๐ŸŽฏ STASH DEBUG: StashDraggableNSImageView: Drag moved to \(screenPoint) - no proximity feedback (stash only)") - } - - func draggingSession(_ session: NSDraggingSession, endedAt screenPoint: NSPoint, operation: NSDragOperation) { - let wasDraggingPreviously = self.isPerformingDrag - isPerformingDrag = false - print("๐Ÿ–ฑ๏ธ STASH DEBUG: StashDraggableNSImageView (ended): Drag session ended. Operation: \(operation.rawValue), wasDraggingPreviously: \(wasDraggingPreviously)") - - // NIEUW: Stop drag session voor proximity monitoring - stashGridManager?.stopDragSession() - print("๐Ÿ–ฑ๏ธ STASH DEBUG: Stopped stash drag session - proximity monitoring activated") - - // AANGEPAST: Laat grid zichtbaar voor interactie, maar start proximity monitoring - if operation != .copy { - print("๐Ÿ”ถ STASH DEBUG: Drag ended with no drop (operation \(operation.rawValue)) - starting proximity monitoring") - print("๐Ÿ”ถ STASH DEBUG: Grid will auto-hide when mouse moves away from stash window") - } else { - print("๐Ÿ”ถ STASH DEBUG: Successful drop (operation \(operation.rawValue)) - grid handled by drop action") - } - - // NIEUW: Delegate notificatie - if let imageURL = self.imageURL { - delegate?.stashImageDragDidEnd(imageURL: imageURL, operation: operation, from: self) - } - - // FIXED: Nu pas mouseDownEvent clearen na drag completion - DispatchQueue.main.async { [weak self] in - self?.mouseDownEvent = nil - print("๐Ÿ–ฑ๏ธ STASH DEBUG: StashDraggableNSImageView (ended): Async cleanup completed for \(self?.imageURL?.lastPathComponent ?? "unknown")") - } - } -} - -// MARK: - SwiftUI Representable Wrapper -struct StashDraggableImageView: NSViewRepresentable { - let nsImage: NSImage - let imageURL: URL - let suggestedName: String? - let stashItem: IdentifiableImage - let delegate: StashDraggableImageViewDelegate - - func makeNSView(context: Context) -> StashDraggableNSImageView { - let nsView = StashDraggableNSImageView() - nsView.image = nsImage - nsView.imageURL = imageURL - nsView.suggestedName = suggestedName - nsView.stashItem = stashItem - nsView.setDelegate(delegate) - return nsView - } - - func updateNSView(_ nsView: StashDraggableNSImageView, context: Context) { - nsView.image = nsImage - nsView.imageURL = imageURL - nsView.suggestedName = suggestedName - nsView.stashItem = stashItem - nsView.setDelegate(delegate) - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/SwiftUIViews.swift b/ShotScreen/Sources/SwiftUIViews.swift deleted file mode 100644 index 692b1e6..0000000 --- a/ShotScreen/Sources/SwiftUIViews.swift +++ /dev/null @@ -1,83 +0,0 @@ -import SwiftUI - -// MARK: - Multi-Monitor Selection Overlay View -struct MultiMonitorSelectionOverlayView: View { - @State private var animationOffset: CGFloat = 0 - - var body: some View { - ZStack { - // Semi-transparent background with subtle gradient - Rectangle() - .fill( - LinearGradient( - gradient: Gradient(colors: [ - Color.blue.opacity(0.15), - Color.blue.opacity(0.25) - ]), - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - - // Animated border - Rectangle() - .stroke( - LinearGradient( - gradient: Gradient(colors: [ - Color.blue.opacity(0.8), - Color.cyan.opacity(0.6), - Color.blue.opacity(0.8) - ]), - startPoint: .leading, - endPoint: .trailing - ), - lineWidth: 2 - ) - .overlay( - // Animated dashed border for better visibility - Rectangle() - .stroke( - Color.white.opacity(0.8), - style: StrokeStyle( - lineWidth: 1, - dash: [8, 4], - dashPhase: animationOffset - ) - ) - ) - - - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .onAppear { - // Start the dash animation - withAnimation(.linear(duration: 2).repeatForever(autoreverses: false)) { - animationOffset = 24 - } - } - } -} - -// MARK: - Crosshair Overlay View -struct CrosshairOverlayView: View { - let mouseLocation: CGPoint - private let crosshairSize: CGFloat = 20 - - var body: some View { - ZStack { - // Horizontal line - Rectangle() - .fill(Color.white) - .frame(width: crosshairSize, height: 2) - .shadow(color: .black, radius: 1, x: 0, y: 0) - - // Vertical line - Rectangle() - .fill(Color.white) - .frame(width: 2, height: crosshairSize) - .shadow(color: .black, radius: 1, x: 0, y: 0) - } - .position(mouseLocation) - .allowsHitTesting(false) // Don't interfere with mouse events - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/ThemeManager.swift b/ShotScreen/Sources/ThemeManager.swift deleted file mode 100644 index b332e9d..0000000 --- a/ShotScreen/Sources/ThemeManager.swift +++ /dev/null @@ -1,176 +0,0 @@ -import AppKit -import SwiftUI - -// MARK: - Centralized Theme Manager -class ThemeManager { - static let shared = ThemeManager() - - private init() {} - - // MARK: - System-Aware Colors (automatic light/dark adaptation) - - /// Background colors for containers (GLASS EFFECT zoals stash) - var containerBackground: NSColor { - // NIEUW: Glass effect achtergrond zoals stash window - // Gebruik transparante achtergrond zodat visual effect view erdoor kan - return NSColor.clear - } - - /// Glass effect background for visual effect views - var glassEffectMaterial: NSVisualEffectView.Material { - return .hudWindow // Exact hetzelfde als stash window - } - - /// Glass effect blending mode - var glassEffectBlending: NSVisualEffectView.BlendingMode { - return .behindWindow // Exact hetzelfde als stash window - } - - /// Glass effect alpha - var glassEffectAlpha: CGFloat { - return 0.95 // Exact hetzelfde als stash window - } - - /// Background colors for secondary containers - var secondaryContainerBackground: NSColor { - if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua { - return NSColor(white: 0.42, alpha: 0.90) // Dark mode: iets lichter - } else { - return NSColor(white: 0.98, alpha: 0.90) // Light mode: bijna wit - } - } - - /// Button and icon colors (adaptive) - var buttonTintColor: NSColor { - if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua { - return NSColor(white: 0.95, alpha: 1.0) // Dark mode: wit - } else { - return NSColor(white: 0.25, alpha: 1.0) // Light mode: donkergrijs - } - } - - /// Text colors (adaptive) - var primaryTextColor: NSColor { - if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua { - return NSColor.white // Dark mode: wit - } else { - return NSColor.black // Light mode: zwart - } - } - - var secondaryTextColor: NSColor { - if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua { - return NSColor(white: 0.7, alpha: 1.0) // Dark mode: grijs - } else { - return NSColor(white: 0.4, alpha: 1.0) // Light mode: donkergrijs - } - } - - /// Filename label text color (adaptive with opacity) - var filenameLabelTextColor: NSColor { - if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua { - return NSColor(white: 0.7, alpha: 1.0) // Dark mode: lichtgrijs - } else { - return NSColor(white: 0.1, alpha: 1.0) // Light mode: veel donkerder voor betere zichtbaarheid - } - } - - /// Grid cell icon background (adaptive) - var gridCellIconBackground: NSColor { - if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua { - return NSColor.white.withAlphaComponent(0.25) // Dark mode - } else { - return NSColor.black.withAlphaComponent(0.25) // Light mode - } - } - - /// Grid cell text color with hover effect - func gridCellTextColor(isHovered: Bool) -> NSColor { - if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua { - return isHovered ? NSColor.white : NSColor.white.withAlphaComponent(0.1) - } else { - return isHovered ? NSColor.black : NSColor.black.withAlphaComponent(0.1) - } - } - - // MARK: - Shadow Configuration (adaptive) - var shadowColor: NSColor { - if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua { - return NSColor.black // Dark mode: black shadows - } else { - return NSColor.black.withAlphaComponent(0.3) // Light mode: lighter shadows - } - } - - var shadowOpacity: Float { - if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua { - return 0.50 // Dark mode: stronger shadows - } else { - return 0.25 // Light mode: subtle shadows - } - } - - // MARK: - Utility Methods - - /// Check if currently in dark mode - var isDarkMode: Bool { - return NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua - } - - /// Debug function to print current theme info - func printCurrentTheme() { - let mode = isDarkMode ? "DARK" : "LIGHT" - print("๐ŸŽจ THEME DEBUG: Current mode = \(mode)") - print("๐ŸŽจ THEME DEBUG: Container background = GLASS EFFECT (\(glassEffectMaterial.rawValue))") - print("๐ŸŽจ THEME DEBUG: Glass effect alpha = \(glassEffectAlpha)") - print("๐ŸŽจ THEME DEBUG: Button tint = \(buttonTintColor)") - print("๐ŸŽจ THEME DEBUG: Primary text = \(primaryTextColor)") - print("๐ŸŽจ THEME DEBUG: Shadow opacity = \(shadowOpacity)") - } - - /// Get hover color for buttons - var buttonHoverColor: NSColor { - if isDarkMode { - return NSColor.white - } else { - return NSColor.black - } - } - - /// Get original button color (for hover restoration) - var buttonOriginalColor: NSColor { - return buttonTintColor - } - - // MARK: - Notification for Theme Changes - func observeThemeChanges(callback: @escaping () -> Void) { - // Observer voor system appearance changes - DistributedNotificationCenter.default.addObserver( - forName: NSNotification.Name("AppleInterfaceThemeChangedNotification"), - object: nil, - queue: .main - ) { _ in - print("๐ŸŽจ THEME: System theme changed - notifying observers") - callback() - } - } -} - -// MARK: - SwiftUI Color Extensions -extension Color { - static var adaptiveContainerBackground: Color { - Color(ThemeManager.shared.containerBackground) - } - - static var adaptiveSecondaryBackground: Color { - Color(ThemeManager.shared.secondaryContainerBackground) - } - - static var adaptivePrimaryText: Color { - Color(ThemeManager.shared.primaryTextColor) - } - - static var adaptiveSecondaryText: Color { - Color(ThemeManager.shared.secondaryTextColor) - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/UpdateManager.swift b/ShotScreen/Sources/UpdateManager.swift deleted file mode 100644 index 9b79e0e..0000000 --- a/ShotScreen/Sources/UpdateManager.swift +++ /dev/null @@ -1,446 +0,0 @@ -import Foundation -import AppKit -import Sparkle - -// MARK: - Update Manager Delegate Protocol -protocol UpdateManagerDelegate: AnyObject { - func updateCheckDidStart() - func updateCheckDidFinish() - func updateAvailable(_ update: SUAppcastItem) - func noUpdateAvailable() - func updateCheckFailed(error: Error) -} - -// MARK: - Custom Update Check Result -struct CustomUpdateCheckResult { - let hasUpdate: Bool - let latestVersion: String? - let currentVersion: String - let downloadURL: String? - let releaseNotes: String? -} - -// MARK: - Update Manager -class UpdateManager: NSObject { - - // MARK: - Properties - static let shared = UpdateManager() - - private var updaterController: SPUStandardUpdaterController? - weak var delegate: UpdateManagerDelegate? - private var isManualCheck = false // Track if this is a manual check - - private let feedURL = "https://git.plet.i234.me/Nick/shotscreen/raw/branch/main/appcast.xml" - - // MARK: - Initialization - override init() { - super.init() - setupUpdater() - } - - // MARK: - Setup - private func setupUpdater() { - print("๐Ÿ”„ UPDATE: Initializing Sparkle updater...") - - // Check if we're in a development build - let isDevelopmentBuild = isRunningInDevelopment() - print("๐Ÿ”ง UPDATE: Development build detected: \(isDevelopmentBuild)") - - // Additional debug info - print("๐Ÿ” UPDATE: Current app version: \(currentVersion)") - print("๐Ÿ” UPDATE: Current build number: \(currentBuildNumber)") - print("๐Ÿ” UPDATE: Bundle path: \(Bundle.main.bundlePath)") - print("๐Ÿ” UPDATE: Bundle URL: \(Bundle.main.bundleURL)") - print("๐Ÿ” UPDATE: Executable path: \(CommandLine.arguments[0])") - - // Initialize Sparkle with standard configuration (for automatic updates only) - updaterController = SPUStandardUpdaterController( - startingUpdater: !isDevelopmentBuild, // Don't auto-start for dev builds - updaterDelegate: self, - userDriverDelegate: nil // Use standard driver for automatic checks - ) - - // Configure the updater - if let updater = updaterController?.updater { - // Configure automatic updates based on user settings and build type - let automaticUpdatesEnabled = !isDevelopmentBuild && SettingsManager.shared.automaticUpdates - updater.automaticallyChecksForUpdates = automaticUpdatesEnabled - updater.automaticallyDownloadsUpdates = false // Always ask user - - print("โœ… UPDATE: Sparkle updater initialized successfully") - print("๐Ÿ”— UPDATE: Feed URL configured via Info.plist: \(feedURL)") - print("๐Ÿค– UPDATE: Automatic checks: \(updater.automaticallyChecksForUpdates)") - print("โš™๏ธ UPDATE: SettingsManager automatic updates: \(SettingsManager.shared.automaticUpdates)") - print("๐Ÿš€ UPDATE: Updater started: \(!isDevelopmentBuild)") - - // Force an immediate update check if this is a manual debug - print("๐Ÿ” UPDATE: Last update check date: \(updater.lastUpdateCheckDate?.description ?? "Never")") - } else { - print("โŒ UPDATE: Failed to access Sparkle updater") - } - } - - // MARK: - Development Detection - private func isRunningInDevelopment() -> Bool { - // Check if we're running from .build directory (swift run) - let executablePath = CommandLine.arguments[0] - let isDevelopment = executablePath.contains(".build") || - executablePath.contains("DerivedData") || - Bundle.main.bundleURL.pathExtension != "app" - - print("๐Ÿ” UPDATE: Executable path: \(executablePath)") - print("๐Ÿ” UPDATE: Bundle URL: \(Bundle.main.bundleURL)") - return isDevelopment - } - - // MARK: - Public Interface - - /// Manually check for updates (triggered by user) - USE SPARKLE'S NATIVE APPROACH - func checkForUpdates() { - print("๐Ÿ” UPDATE: === checkForUpdates() START ===") - print("๐Ÿ” UPDATE: updaterController exists: \(updaterController != nil)") - - if let controller = updaterController { - print("๐Ÿ” UPDATE: updaterController.updater exists: true") - } - - guard let updater = updaterController?.updater else { - print("โŒ UPDATE: Updater not available - showing fallback") - showManualUpdateFallback() - return - } - - print("๐Ÿ” UPDATE: Manual update check triggered (using Sparkle's native method)") - print("๐Ÿ” UPDATE: Current version from bundle: \(currentVersion)") - print("๐Ÿ” UPDATE: Current build number: \(currentBuildNumber)") - print("๐Ÿ” UPDATE: Feed URL: \(feedURL)") - print("๐Ÿ” UPDATE: Automatic checks enabled: \(updater.automaticallyChecksForUpdates)") - print("๐Ÿ” UPDATE: Last check date: \(updater.lastUpdateCheckDate?.description ?? "Never")") - - isManualCheck = true - delegate?.updateCheckDidStart() - - print("๐Ÿ” UPDATE: About to call updater.checkForUpdates()") - // Use Sparkle's own checkForUpdates method (this should work reliably) - updater.checkForUpdates() - print("๐Ÿ” UPDATE: updater.checkForUpdates() called successfully") - } - - // MARK: - Custom Update Check Implementation (REMOVED - now only used as fallback) - - private func performCustomUpdateCheckFallback() { - guard let url = URL(string: feedURL) else { - print("โŒ UPDATE: Invalid feed URL") - delegate?.updateCheckFailed(error: NSError(domain: "UpdateManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid feed URL"])) - isManualCheck = false - return - } - - print("๐Ÿ” UPDATE: Performing fallback update check from: \(feedURL)") - - // Create URL session - let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in - DispatchQueue.main.async { - self?.handleCustomUpdateCheckResponse(data: data, response: response, error: error) - } - } - task.resume() - } - - private func handleCustomUpdateCheckResponse(data: Data?, response: URLResponse?, error: Error?) { - defer { - isManualCheck = false - delegate?.updateCheckDidFinish() - } - - if let error = error { - print("โŒ UPDATE: Network error: \(error.localizedDescription)") - delegate?.updateCheckFailed(error: error) - return - } - - guard let data = data else { - print("โŒ UPDATE: No data received") - let error = NSError(domain: "UpdateManager", code: -2, userInfo: [NSLocalizedDescriptionKey: "No data received"]) - delegate?.updateCheckFailed(error: error) - return - } - - print("โœ… UPDATE: Received appcast data (\(data.count) bytes)") - - // Parse the appcast XML - parseAppcastAndCheckForUpdates(data: data) - } - - private func parseAppcastAndCheckForUpdates(data: Data) { - do { - let xml = try XMLDocument(data: data, options: []) - - // Find the latest item in the appcast - guard let latestItem = xml.rootElement()?.elements(forName: "channel").first?.elements(forName: "item").first else { - print("โŒ UPDATE: No items found in appcast") - delegate?.noUpdateAvailable() - return - } - - // Extract version information - guard let enclosure = latestItem.elements(forName: "enclosure").first, - let versionAttribute = enclosure.attribute(forName: "sparkle:version"), - let latestVersionString = versionAttribute.stringValue else { - print("โŒ UPDATE: Could not extract version from appcast") - delegate?.noUpdateAvailable() - return - } - - let currentVersionString = currentVersion - - print("๐Ÿ” UPDATE: Current version: \(currentVersionString)") - print("๐Ÿ” UPDATE: Latest version: \(latestVersionString)") - - // Compare versions - let hasUpdate = compareVersions(current: currentVersionString, latest: latestVersionString) - - if hasUpdate { - print("๐ŸŽ‰ UPDATE: New version available!") - - // Create a mock SUAppcastItem for compatibility - if let mockItem = createMockAppcastItem(from: latestItem, version: latestVersionString) { - delegate?.updateAvailable(mockItem) - } else { - // Fallback to custom update available notification - showCustomUpdateAvailable(latestVersion: latestVersionString, currentVersion: currentVersionString) - } - } else { - print("โ„น๏ธ UPDATE: No update available (current: \(currentVersionString), latest: \(latestVersionString))") - delegate?.noUpdateAvailable() - } - - } catch { - print("โŒ UPDATE: XML parsing error: \(error.localizedDescription)") - delegate?.updateCheckFailed(error: error) - } - } - - private func compareVersions(current: String, latest: String) -> Bool { - // Handle "Unknown" version (development builds) - if current == "Unknown" { - print("๐Ÿ” UPDATE: Skipping update check for development build") - return false // Don't offer updates for development builds - } - - print("๐Ÿ” UPDATE: Comparing versions - Current: '\(current)' vs Latest: '\(latest)'") - - // Simple version comparison (assuming semantic versioning like "1.0", "1.1", etc.) - let currentComponents = current.split(separator: ".").compactMap { Int($0) } - let latestComponents = latest.split(separator: ".").compactMap { Int($0) } - - print("๐Ÿ” UPDATE: Current components: \(currentComponents)") - print("๐Ÿ” UPDATE: Latest components: \(latestComponents)") - - // Pad arrays to same length - let maxLength = max(currentComponents.count, latestComponents.count) - var currentPadded = currentComponents - var latestPadded = latestComponents - - while currentPadded.count < maxLength { currentPadded.append(0) } - while latestPadded.count < maxLength { latestPadded.append(0) } - - print("๐Ÿ” UPDATE: Current padded: \(currentPadded)") - print("๐Ÿ” UPDATE: Latest padded: \(latestPadded)") - - // Compare component by component - for i in 0.. currentPadded[i] { - print("๐ŸŽ‰ UPDATE: Found newer version at component \(i): \(latestPadded[i]) > \(currentPadded[i])") - return true - } else if latestPadded[i] < currentPadded[i] { - print("โš ๏ธ UPDATE: Latest version is older at component \(i): \(latestPadded[i]) < \(currentPadded[i])") - return false - } - } - - print("โ„น๏ธ UPDATE: Versions are equal") - return false // Versions are equal - } - - private func createMockAppcastItem(from xmlElement: XMLElement, version: String) -> SUAppcastItem? { - // This is a bit tricky as SUAppcastItem is not easily creatable - // For now, we'll use our custom update available method - return nil - } - - private func showCustomUpdateAvailable(latestVersion: String, currentVersion: String) { - DispatchQueue.main.async { [weak self] in - let alert = NSAlert() - alert.messageText = "Update Available! ๐ŸŽ‰" - alert.informativeText = """ - ShotScreen \(latestVersion) is now available. - You have version \(currentVersion). - - Would you like to visit the releases page to download it? - """ - alert.addButton(withTitle: "Download Update") - alert.addButton(withTitle: "Skip This Version") - alert.addButton(withTitle: "Remind Me Later") - alert.alertStyle = .informational - - // Set app icon - if let appIcon = NSApplication.shared.applicationIconImage { - alert.icon = appIcon - } - - let response = alert.runModal() - switch response { - case .alertFirstButtonReturn: - print("โœ… UPDATE: User chose to download update") - if let url = URL(string: "https://git.plet.i234.me/Nick/shotscreen/releases") { - NSWorkspace.shared.open(url) - } - case .alertSecondButtonReturn: - print("โŒ UPDATE: User chose to skip this version") - case .alertThirdButtonReturn: - print("โฐ UPDATE: User chose to be reminded later") - default: - break - } - } - } - - /// Update automatic update settings - func updateAutomaticUpdateSettings() { - guard let updater = updaterController?.updater else { return } - - let automaticUpdates = SettingsManager.shared.automaticUpdates - updater.automaticallyChecksForUpdates = automaticUpdates - - print("โš™๏ธ UPDATE: Automatic updates \(automaticUpdates ? "enabled" : "disabled")") - } - - /// Check if updater is available - var isUpdaterAvailable: Bool { - return updaterController?.updater != nil - } - - /// Get current app version - var currentVersion: String { - return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Unknown" - } - - /// Get current build number - var currentBuildNumber: String { - return Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "Unknown" - } - - // MARK: - Private Methods - - private func showManualUpdateFallback() { - print("๐Ÿ”„ UPDATE: Showing manual update fallback") - - let alert = NSAlert() - alert.messageText = "Check for Updates" - alert.informativeText = "Automatic update checking is currently unavailable. You can check for updates manually on our releases page." - alert.addButton(withTitle: "Open Releases Page") - alert.addButton(withTitle: "Cancel") - alert.alertStyle = .informational - - let response = alert.runModal() - if response == .alertFirstButtonReturn { - if let url = URL(string: "https://git.plet.i234.me/Nick/shotscreen/releases") { - NSWorkspace.shared.open(url) - } - } - } - - // MARK: - Debug Info - - func printDebugInfo() { - print("\n๐Ÿ” UPDATE DEBUG INFO:") - print("๐Ÿ“ฑ Current Version: \(currentVersion)") - print("๐Ÿ”ข Build Number: \(currentBuildNumber)") - print("๐Ÿ”— Feed URL: \(feedURL)") - print("โšก Updater Available: \(isUpdaterAvailable)") - print("๐Ÿค– Automatic Updates: \(SettingsManager.shared.automaticUpdates)") - - if let updater = updaterController?.updater { - print("๐Ÿ“… Last Check: \(updater.lastUpdateCheckDate?.description ?? "Never")") - } - print("๐Ÿ” UPDATE DEBUG INFO END\n") - } - - /// Force a debug update check - bypasses development detection temporarily - func forceDebugUpdateCheck() { - print("๐Ÿ”ง DEBUG: Forcing update check (bypassing development detection)") - - // Temporarily create a new updater controller that ignores development mode - let debugController = SPUStandardUpdaterController( - startingUpdater: true, // Always start for debug - updaterDelegate: self, - userDriverDelegate: nil - ) - - let debugUpdater = debugController.updater - print("๐Ÿ”ง DEBUG: Debug updater created successfully") - debugUpdater.automaticallyChecksForUpdates = false - debugUpdater.automaticallyDownloadsUpdates = false - - isManualCheck = true - delegate?.updateCheckDidStart() - - print("๐Ÿ”ง DEBUG: Starting forced update check...") - debugUpdater.checkForUpdates() - } -} - -// MARK: - SPUUpdaterDelegate (automatic updates only - manual updates use custom implementation) -extension UpdateManager: @preconcurrency SPUUpdaterDelegate { - - func updater(_ updater: SPUUpdater, didFinishUpdateCycleFor updateCheck: SPUUpdateCheck, error: Error?) { - // This is only called for automatic background checks now (not manual checks) - print("โœ… UPDATE: Automatic update check completed") - - if let error = error { - print("โŒ UPDATE: Automatic update check failed: \(error.localizedDescription)") - } - - // Let Sparkle handle everything - no delegate calls to prevent UI interference - isManualCheck = false - } - - func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) { - print("๐ŸŽ‰ UPDATE: Sparkle found valid update!") - print("๐Ÿ” UPDATE: Update version: \(item.displayVersionString)") - print("๐Ÿ” UPDATE: Current version: \(currentVersion)") - print("๐Ÿ” UPDATE: Update info: \(item.infoURL?.absoluteString ?? "None")") - print("๐Ÿ” UPDATE: Was manual check: \(isManualCheck)") - - // Let Sparkle handle the UI completely - don't trigger additional dialogs - // Just notify delegate for internal state tracking (no additional UI) - isManualCheck = false - // delegate?.updateAvailable(item) // Commented out to prevent double UI - } - - func updaterDidNotFindUpdate(_ updater: SPUUpdater) { - print("โ„น๏ธ UPDATE: No updates found by Sparkle") - print("๐Ÿ” UPDATE: Current version: \(currentVersion)") - print("๐Ÿ” UPDATE: Feed URL used: \(feedURL)") - print("๐Ÿ” UPDATE: Was manual check: \(isManualCheck)") - - // Let Sparkle handle the "no update" UI completely - don't add our own popup - // Sparkle already shows its own "You're up to date!" dialog for manual checks - - isManualCheck = false - // delegate?.noUpdateAvailable() // Commented out to prevent multiple dialogs - } - - func updater(_ updater: SPUUpdater, willInstallUpdate item: SUAppcastItem) { - print("๐Ÿ“ฆ UPDATE: Will install update: \(item.displayVersionString)") - } - - func updater(_ updater: SPUUpdater, didAbortWithError error: Error) { - print("๐Ÿ’ฅ UPDATE: Update aborted with error: \(error.localizedDescription)") - - // For automatic updates, we don't need to notify the delegate of failures - // since they're silent background operations - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/WindowCaptureManager.swift b/ShotScreen/Sources/WindowCaptureManager.swift deleted file mode 100644 index 77a6d8d..0000000 --- a/ShotScreen/Sources/WindowCaptureManager.swift +++ /dev/null @@ -1,998 +0,0 @@ -import AppKit -import ScreenCaptureKit -import SwiftUI -import UniformTypeIdentifiers -import ObjectiveC - -// MARK: - Window Capture Manager -@available(macOS 12.3, *) -class WindowCaptureManager: NSObject, ObservableObject { - - // MARK: - Properties - weak var screenshotApp: ScreenshotApp? - var determinedMainScreen: NSScreen! // Stores the true main screen for the current selection session - - // Available content for capture - @Published var availableWindows: [SCWindow] = [] - @Published var availableDisplays: [SCDisplay] = [] - @Published var isCapturing = false - @Published var captureError: String? - - // Window selection state - @Published var isWindowSelectionActive = false - private var windowSelectionOverlay: WindowSelectionOverlay? - private var globalMouseMonitor: Any? - - // MARK: - Initialization - override init() { - super.init() - } - - convenience init(screenshotApp: ScreenshotApp) { - self.init() - self.screenshotApp = screenshotApp - } - - // MARK: - Content Discovery - func refreshAvailableContent() async { - print("๐Ÿ”„ Refreshing available content for window capture...") - - do { - let shareableContent = try await SCShareableContent.current - await updateAvailableContent(shareableContent) - print("โœ… Content refreshed successfully") - } catch { - await handleContentRefreshError(error) - } - } - - // MARK: - Private Helper Methods for Content Discovery - - private func updateAvailableContent(_ shareableContent: SCShareableContent) async { - let filteredWindows = filterShareableWindows(shareableContent.windows) - let validDisplays = shareableContent.displays.filter { $0.width > 0 && $0.height > 0 } - - await MainActor.run { - self.availableWindows = filteredWindows - self.availableDisplays = validDisplays - self.captureError = nil - } - - logContentSummary(windows: filteredWindows, displays: validDisplays) - } - - private func filterShareableWindows(_ windows: [SCWindow]) -> [SCWindow] { - return windows.filter { window in - // Filter criteria for capturable windows - guard window.isOnScreen, - window.frame.width > 50, - window.frame.height > 50, - let app = window.owningApplication, - app.applicationName != "WindowServer", - app.applicationName != "Dock" else { - return false - } - - // Exclude our own app's windows - if let ourBundleID = Bundle.main.bundleIdentifier, - app.bundleIdentifier == ourBundleID { - return false - } - - return true - } - } - - private func logContentSummary(windows: [SCWindow], displays: [SCDisplay]) { - print("๐Ÿ“Š Content Summary:") - print(" Windows: \(windows.count) capturable windows found") - print(" Displays: \(displays.count) displays found") - - if windows.count > 0 { - print("๐ŸชŸ Sample windows:") - for (index, window) in windows.prefix(5).enumerated() { - let app = window.owningApplication?.applicationName ?? "Unknown" - let title = window.title?.isEmpty == false ? window.title! : "No Title" - print(" \(index + 1). \(title) (\(app)) - Layer: \(window.windowLayer)") - } - if windows.count > 5 { - print(" ... and \(windows.count - 5) more windows") - } - } - } - - private func handleContentRefreshError(_ error: Error) async { - await MainActor.run { - self.captureError = "Failed to get shareable content: \(error.localizedDescription)" - print("โŒ WindowCaptureManager Error: \(error)") - } - } - - // MARK: - Window Selection Mode - func activateWindowSelectionMode() { - print("๐ŸŽฌ ACTIVATE: Starting window selection mode...") - print("๐ŸชŸ Activating window selection mode") - - // Determine the actual main screen (with origin 0,0) for consistent coordinate calculations - self.determinedMainScreen = NSScreen.screens.first { $0.frame.origin == .zero } - if self.determinedMainScreen == nil { - print("โš ๏ธ CRITICAL: Could not find screen with origin (0,0). Falling back to default NSScreen.main.") - self.determinedMainScreen = NSScreen.main! // Fallback, though this might be the unreliable one - } - print("โœจ Using determined main screen for this session: \(determinedMainScreen.customLocalizedName) with frame \(determinedMainScreen.frame)") - - print("๐ŸŽฌ ACTIVATE: Starting async task to refresh content and show overlay...") - - Task { - print("๐ŸŽฌ TASK: Inside async task, refreshing content...") - await refreshAvailableContent() - await MainActor.run { - print("๐ŸŽฌ TASK: Content refreshed, setting flags and showing overlay...") - self.isWindowSelectionActive = true - self.showWindowSelectionOverlay() - print("๐ŸŽฌ TASK: Window selection overlay should now be shown") - } - } - } - - // MARK: - Window Selection Mode Deactivation - func deactivateWindowSelectionMode() { - print("๐ŸชŸ Deactivating window selection mode") - - isWindowSelectionActive = false - hideWindowSelectionOverlay() - - // Remove any mouse monitors - if let monitor = globalMouseMonitor { - NSEvent.removeMonitor(monitor) - globalMouseMonitor = nil - } - } - - private func showWindowSelectionOverlay() { - // Create overlay window that shows available windows - let overlay = WindowSelectionOverlay(windowCaptureManager: self) - overlay.show() - windowSelectionOverlay = overlay - } - - private func hideWindowSelectionOverlay() { - windowSelectionOverlay?.hide() - windowSelectionOverlay = nil - } - - private func setupWindowSelectionMonitoring() { - globalMouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown]) { [weak self] event in - guard let self = self, self.isWindowSelectionActive else { return } - - let location = NSEvent.mouseLocation - self.handleWindowSelection(at: location) - } - } - - private func removeWindowSelectionMonitoring() { - if let monitor = globalMouseMonitor { - NSEvent.removeMonitor(monitor) - globalMouseMonitor = nil - } - } - - private func handleWindowSelection(at location: NSPoint) { - // HERSTEL: Zoek naar een window - if let window = findWindowAt(location: location) { - let windowTitleForPrint = window.title ?? "Untitled" - print("๐ŸŽฏ Window selected: \(windowTitleForPrint) at \(location)") - deactivateWindowSelectionMode() - - Task { - await captureWindow(window) // Gebruik de captureWindow methode - } - } else { - print(" No window found at \(location), canceling") - deactivateWindowSelectionMode() - } - } - - // MARK: - Window Detection - func findWindowAt(location: NSPoint) -> SCWindow? { - NSLog("๐ŸŽฏ Finding window at global location: %@", NSStringFromPoint(location)) - - // Step 1: Validate main screen reference - guard let mainScreenRef = validateMainScreenReference() else { - return nil - } - - // Step 2: Find the screen containing the mouse location - guard let mouseScreen = findScreenContaining(location: location) else { - return nil - } - - // Step 3: Search for window at location - return searchWindowsAt(location: location, mouseScreen: mouseScreen, mainScreenRef: mainScreenRef) - } - - // MARK: - Private Helper Methods for Window Detection - - private func validateMainScreenReference() -> NSScreen? { - guard let mainScreenRef = self.determinedMainScreen else { - NSLog("โŒ No determined main screen available for findWindowAt") - return nil - } - return mainScreenRef - } - - private func findScreenContaining(location: NSPoint) -> NSScreen? { - for screen in NSScreen.screens { - if screen.frame.contains(location) { - NSLog("๐ŸŽฏ Mouse is on NSScreen: '%@' (Frame: %@)", screen.customLocalizedName, NSStringFromRect(screen.frame)) - return screen - } - } - NSLog("โŒ Mouse location not on any NSScreen: %@", NSStringFromPoint(location)) - return nil - } - - private func searchWindowsAt(location: NSPoint, mouseScreen: NSScreen, mainScreenRef: NSScreen) -> SCWindow? { - // Sort windows by layer, so frontmost windows are checked first - let sortedWindows = availableWindows.sorted { $0.windowLayer > $1.windowLayer } - NSLog("๐ŸŽฏ Checking \(sortedWindows.count) sorted windows.") - - for window in sortedWindows { - if let foundWindow = checkWindowAtLocation(window: window, location: location, - mouseScreen: mouseScreen, mainScreenRef: mainScreenRef) { - return foundWindow - } - } - - NSLog(" โŒ No window found at global location: %@ on screen %@", NSStringFromPoint(location), mouseScreen.customLocalizedName) - return nil - } - - private func checkWindowAtLocation(window: SCWindow, location: NSPoint, - mouseScreen: NSScreen, mainScreenRef: NSScreen) -> SCWindow? { - let scWinFrame = window.frame - let globalWindowFrame = self.convertSCWindowFrameToGlobal(scWinFrame, mainScreen: mainScreenRef) - - // Check 1: Does the window intersect with the screen the mouse is on? - guard mouseScreen.frame.intersects(globalWindowFrame) else { - return nil - } - - // Check 2: Does the global window frame contain the mouse location? - guard globalWindowFrame.contains(location) else { - return nil - } - - NSLog(" ๐ŸŽ‰ FOUND window by global coords: '%@' (App: %@) on screen '%@'", - window.title ?? "Untitled", - window.owningApplication?.applicationName ?? "Unknown", - mouseScreen.customLocalizedName) - return window - } - - // MARK: - Window Capture Methods - - /// Capture a specific window by SCWindow object - func captureWindow(_ window: SCWindow) async { - print("๐Ÿ“ธ WindowCaptureManager: Capturing window: \(window.title ?? "Untitled") using ScreenCaptureKitProvider") - - // Step 1: Initialize capture state - await initializeCaptureState() - - // Step 2: Validate screen capture provider - guard let provider = validateScreenCaptureProvider() else { - await handleCaptureError("ScreenCaptureProvider not available.", code: 3) - return - } - - // Step 3: Perform capture - await performWindowCapture(window: window, provider: provider) - } - - // MARK: - Private Helper Methods for Window Capture - - private func initializeCaptureState() async { - await MainActor.run { - isCapturing = true - captureError = nil - } - } - - private func validateScreenCaptureProvider() -> ScreenCaptureKitProvider? { - return self.screenshotApp?.screenCaptureProvider - } - - private func performWindowCapture(window: SCWindow, provider: ScreenCaptureKitProvider) async { - if let image = await provider.captureWindow(window: window) { - await handleSuccessfulCapture(image: image) - } else { - await handleCaptureError("Failed to capture window with ScreenCaptureKitProvider.", code: 2) - } - } - - private func handleSuccessfulCapture(image: NSImage) async { - await MainActor.run { - self.isCapturing = false - self.screenshotApp?.processCapture(image: image) - self.deactivateWindowSelectionMode() - print("โœ… Window captured successfully and selection mode deactivated.") - } - } - - private func handleCaptureError(_ description: String, code: Int) async { - await MainActor.run { - self.isCapturing = false - self.captureError = NSError(domain: "WindowCaptureError", code: code, - userInfo: [NSLocalizedDescriptionKey: description]).localizedDescription - self.deactivateWindowSelectionMode() - print("Error: \(description)") - } - } - - // MARK: - Core Graphics Capture Method (Old - Replaced by ScreenCaptureKitProvider) - /* - private func captureWindowWithCoreGraphics(_ window: SCWindow) async { - let windowID = window.windowID - let imageRef = CGWindowListCreateImage(.null, .optionIncludingWindow, windowID, .bestResolution) - - guard let cgImage = imageRef else { - await MainActor.run { - self.captureError = "Failed to capture window" - self.isCapturing = false - } - return - } - - let nsImage = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height)) - - await MainActor.run { - self.isCapturing = false - // Process the captured image through the main app - self.screenshotApp?.processWindowCapture(image: nsImage, windowTitle: window.title) - } - } - */ - - /// Capture window by application name - func captureWindowByApp(appName: String) async { - await refreshAvailableContent() - - guard let window = availableWindows.first(where: { - $0.owningApplication?.applicationName == appName - }) else { - await MainActor.run { - self.captureError = "No window found for app: \(appName)" - } - return - } - - await captureWindow(window) - } - - /// Capture frontmost window - func captureFrontmostWindow() async { - await refreshAvailableContent() - - // Get the frontmost window (highest window level) - guard let frontmostWindow = availableWindows.max(by: { $0.windowLayer < $1.windowLayer }) else { - await MainActor.run { - self.captureError = "No frontmost window found" - } - return - } - - await captureWindow(frontmostWindow) - } - - /// Capture window containing a specific point - func captureWindowAt(point: NSPoint) async { - await refreshAvailableContent() - - guard let window = findWindowAt(location: point) else { - await MainActor.run { - self.captureError = "No window found at specified location" - } - return - } - - await captureWindow(window) - } - - // MARK: - Settings Integration - func getWindowCaptureSettings() -> WindowCaptureSettings { - return WindowCaptureSettings( - includeCursor: UserDefaults.standard.bool(forKey: "windowCaptureIncludeCursor"), - showSelectionUI: UserDefaults.standard.bool(forKey: "windowCaptureShowSelectionUI") - ) - } - - func saveWindowCaptureSettings(_ settings: WindowCaptureSettings) { - UserDefaults.standard.set(settings.includeCursor, forKey: "windowCaptureIncludeCursor") - UserDefaults.standard.set(settings.showSelectionUI, forKey: "windowCaptureShowSelectionUI") - } - - // MARK: - Coordinate Conversion Helper - // Converts an SCWindow.frame (origin top-left of main screen, Y-down) to Global coordinates (origin bottom-left of main screen, Y-up) - func convertSCWindowFrameToGlobal(_ scWindowFrame: CGRect, mainScreen: NSScreen) -> CGRect { - let globalX = scWindowFrame.origin.x - // Y of top edge of scWindow in Global Y-up (from main screen bottom) - let globalY_topEdge = mainScreen.frame.height - scWindowFrame.origin.y - // Y of bottom edge of scWindow in Global Y-up (this becomes the origin for the CGRect) - let globalY_bottomEdge_forOrigin = globalY_topEdge - scWindowFrame.height - - return CGRect(x: globalX, y: globalY_bottomEdge_forOrigin, width: scWindowFrame.width, height: scWindowFrame.height) - } - - // MARK: - Window Detection (Wordt Display Detection) - func findDisplayAt(location: NSPoint) -> SCDisplay? { - NSLog("๐ŸŽฏ Finding display at global location: %@", NSStringFromPoint(location)) - - // We gebruiken de beschikbare SCDisplay objecten direct. - // Hun frames zijn in pixels, oorsprong linksboven van *dat specifieke display*. - // Om te checken of de muis (globale punten, oorsprong linksonder hoofdmenu) binnen een display valt, - // moeten we de SCDisplay.frame converteren naar globale punten, of de muislocatie naar de coรถrdinaten van elk display. - // Makkelijker: gebruik NSScreen.screens, vind de NSScreen waar de muis op is, en zoek dan de SCDisplay met dezelfde displayID. - - var currentMouseNSScreen: NSScreen? - for s in NSScreen.screens { - if s.frame.contains(location) { - currentMouseNSScreen = s - break - } - } - guard let mouseNSScreen = currentMouseNSScreen else { - NSLog("โŒ Mouse location %@ not on any NSScreen.", NSStringFromPoint(location)) - return nil - } - NSLog("๐ŸŽฏ Mouse is on NSScreen: '%@' (Frame: %@), DisplayID: \(mouseNSScreen.displayID)", mouseNSScreen.customLocalizedName, NSStringFromRect(mouseNSScreen.frame)) - - // Zoek de SCDisplay die overeenkomt met deze NSScreen - if let matchedDisplay = availableDisplays.first(where: { $0.displayID == mouseNSScreen.displayID }) { - NSLog(" ๐ŸŽ‰ FOUND display by ID match: DisplayID \(matchedDisplay.displayID)") - return matchedDisplay - } - - NSLog(" โŒ No SCDisplay found matching NSScreen with DisplayID \(mouseNSScreen.displayID) at global location: %@", NSStringFromPoint(location)) - return nil - } - - // MARK: - Window Capture Methods (Wordt Display Capture Methods) - - /// Capture a specific display by SCDisplay object - func captureDisplay(_ display: SCDisplay) async { - print("๐Ÿ–ฅ๏ธ WindowCaptureManager: Capturing display ID \(display.displayID) using ScreenCaptureKitProvider") - - // Converteer SCDisplay naar NSScreen - guard let targetNSScreen = NSScreen.screens.first(where: { $0.displayID == display.displayID }) else { - await MainActor.run { - self.isCapturing = false - self.captureError = NSError(domain: "DisplayCaptureError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Could not find NSScreen matching SCDisplay ID \(display.displayID)."]).localizedDescription - self.deactivateWindowSelectionMode() - print("Error: Could not find NSScreen for SCDisplay ID \(display.displayID).") - } - return - } - print("๐Ÿ–ฅ๏ธ Corresponderende NSScreen gevonden: \(targetNSScreen.customLocalizedName)") - - await MainActor.run { - isCapturing = true - captureError = nil - } - - if let provider = self.screenshotApp?.screenCaptureProvider { - // ScreenCaptureKitProvider.captureScreen verwacht een NSScreen - // De excludingWindows parameter is optioneel en wordt hier nil gelaten, - // omdat we het hele scherm capturen en desktop icons apart worden behandeld in de provider. - if let image = await provider.captureScreen(screen: targetNSScreen, excludingWindows: nil) { - await MainActor.run { - self.isCapturing = false - self.screenshotApp?.processCapture(image: image) - self.deactivateWindowSelectionMode() - print("โœ… Display captured successfully and selection mode deactivated.") - } - } else { - await MainActor.run { - self.isCapturing = false - self.captureError = NSError(domain: "DisplayCaptureError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to capture display with ScreenCaptureKitProvider."]).localizedDescription - self.deactivateWindowSelectionMode() - print("Error: Failed to capture display ID \(display.displayID) with ScreenCaptureKitProvider.") - } - } - } else { - await MainActor.run { - self.isCapturing = false - self.captureError = NSError(domain: "DisplayCaptureError", code: 3, userInfo: [NSLocalizedDescriptionKey: "ScreenCaptureProvider not available."]).localizedDescription - self.deactivateWindowSelectionMode() - print("Error: ScreenCaptureProvider not available for display capture.") - } - } - } -} - -// MARK: - Window Capture Settings -struct WindowCaptureSettings { - var includeCursor: Bool = true - var showSelectionUI: Bool = true -} - -// MARK: - Window Selection Overlay -@available(macOS 12.3, *) -class WindowSelectionOverlay: NSObject { - private var windowCaptureManager: WindowCaptureManager - private var overlayWindows: [NSWindow] = [] // Dit zijn de semi-transparante overlay windows per scherm - private var globalMouseMonitor: Any? - - init(windowCaptureManager: WindowCaptureManager) { - self.windowCaptureManager = windowCaptureManager - super.init() - } - - func show() { - print("๐ŸŽฌ OVERLAY SHOW: Starting to create overlays...") - - hide() // Clean up any existing overlays - - print("๐ŸชŸ WindowCaptureManager: Starting window selection on \(NSScreen.screens.count) screens") - for (index, screen) in NSScreen.screens.enumerated() { - print(" Screen \(index): \(screen.customLocalizedName) - Frame: \(screen.frame)") - } - - // Create a separate overlay window for each screen - for (index, screen) in NSScreen.screens.enumerated() { - print("๐ŸŽฌ OVERLAY SHOW: Creating overlay for screen \(index): \(screen.customLocalizedName)") - let overlayWindow = createOverlayWindow(for: screen) - overlayWindows.append(overlayWindow) - - print("๐ŸŽฌ OVERLAY SHOW: Window created, ordering front...") - - // IMPROVED: Proper window ordering and activation - overlayWindow.orderFrontRegardless() - overlayWindow.makeKeyAndOrderFront(nil) - - print("๐ŸŽฌ OVERLAY SHOW: Window ordered front, checking content view...") - - // Force the content view to display immediately - if let contentView = overlayWindow.contentView { - print("๐ŸŽฌ OVERLAY SHOW: Content view found, forcing display...") - contentView.needsDisplay = true - contentView.displayIfNeeded() - print("๐ŸŽจ FORCED DISPLAY for screen \(screen.customLocalizedName)") - } else { - print("โŒ OVERLAY SHOW: NO CONTENT VIEW found for screen \(screen.customLocalizedName)") - } - } - - print("๐ŸŽฌ OVERLAY SHOW: All overlays created, setting up mouse monitoring...") - - // Setup global mouse monitoring for all screens - setupGlobalMouseMonitoring() - - // Force app activation - NSApp.activate(ignoringOtherApps: true) - - print("๐ŸชŸ Window selection overlay shown on \(overlayWindows.count) screens") - - // ADDITIONAL: Force a refresh after a short delay - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - print("๐ŸŽฌ DELAYED REFRESH: Starting...") - for (index, window) in self.overlayWindows.enumerated() { - if let contentView = window.contentView { - contentView.needsDisplay = true - print("๐ŸŽจ DELAYED REFRESH for window \(index)") - } else { - print("โŒ DELAYED REFRESH: NO CONTENT VIEW for window \(index)") - } - } - } - - // Debug: List available windows with their positions - print("๐ŸชŸ Available windows for capture:") - for (index, window) in windowCaptureManager.availableWindows.enumerated() { - let frame = window.frame - let app = window.owningApplication?.applicationName ?? "Unknown" - print(" \(index): \(window.title ?? "Untitled") (\(app)) - Frame: \(frame)") - } - } - - func hide() { - // Remove all overlay windows - for window in overlayWindows { - window.orderOut(nil) - } - overlayWindows.removeAll() - - // Remove global mouse monitor - if let monitor = globalMouseMonitor { - NSEvent.removeMonitor(monitor) - globalMouseMonitor = nil - } - - print("๐ŸชŸ Window selection overlay hidden") - } - - private func createOverlayWindow(for screen: NSScreen) -> NSWindow { - let window = NSWindow( - contentRect: screen.frame, - styleMask: [.borderless, .fullSizeContentView], - backing: .buffered, - defer: false - ) - - // FIXED: Window configuration for proper drawing and event handling - window.backgroundColor = NSColor.clear // Clear background so we can draw our own - window.isOpaque = false - window.level = NSWindow.Level.floating // Lower level but still above normal windows - window.ignoresMouseEvents = false - window.acceptsMouseMovedEvents = true - window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .ignoresCycle] - - // CRITICAL: Ensure window can receive events and display properly - window.displaysWhenScreenProfileChanges = true - window.animationBehavior = .none - window.hidesOnDeactivate = false - - // IMPORTANT: Enable automatic invalidation for proper drawing - window.invalidateShadow() - window.viewsNeedDisplay = true - - // Create content view for this screen - let overlayView = WindowSelectionOverlayView( - windowCaptureManager: windowCaptureManager, - screen: screen - ) - - // CRITICAL: Properly configure the view for drawing - overlayView.wantsLayer = false // Use direct drawing for better control - overlayView.needsDisplay = true - - window.contentView = overlayView - - // Position window exactly on this screen - window.setFrame(screen.frame, display: true) // Force display update - - print("๐ŸชŸ Created overlay window for screen: \(screen.customLocalizedName) at level \(window.level.rawValue)") - print("๐ŸชŸ Window frame: \(window.frame)") - print("๐ŸชŸ Content view frame: \(overlayView.frame)") - print("๐ŸชŸ Content view needsDisplay: \(overlayView.needsDisplay)") - - return window - } - - private func setupGlobalMouseMonitoring() { - globalMouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .keyDown]) { [weak self] event in - guard let self = self else { return } - - if event.type == .leftMouseDown { - let location = NSEvent.mouseLocation - print("๐ŸŽฏ Global mouse click at: \(location) for window capture") - print("๐Ÿ”ฅ DEBUG: Available windows: \(self.windowCaptureManager.availableWindows.count)") - - // ๐Ÿ”ฅ FIXED: In window capture mode, any click should capture the window - print("๐ŸชŸ Click detected in window capture mode - capturing window") - self.handleMouseClick(at: location) - } else if event.type == .keyDown && event.keyCode == 53 { // ESC - print("โŒจ๏ธ ESC key pressed, canceling window/display capture") - self.windowCaptureManager.deactivateWindowSelectionMode() - } - } - - print("๐ŸŽฏ Global mouse monitoring setup for window capture") - } - - private func handleMouseClick(at location: NSPoint) { - // HERSTEL: Zoek naar een window - if let window = windowCaptureManager.findWindowAt(location: location) { - let selectedWindowTitle = window.title ?? "Untitled" - print("๐ŸŽฏ Window selected: \(selectedWindowTitle) at \(location)") - windowCaptureManager.deactivateWindowSelectionMode() - - Task { - await windowCaptureManager.captureWindow(window) // Gebruik de captureWindow methode - } - } else { - print(" No window found at \(location), canceling") - windowCaptureManager.deactivateWindowSelectionMode() - } - } -} - -// MARK: - Window Selection Overlay View -@available(macOS 12.3, *) -class WindowSelectionOverlayView: NSView { - private var windowCaptureManager: WindowCaptureManager - private var screen: NSScreen // Het NSScreen object waar deze view op getekend wordt - private var hoveredWindow: SCWindow? // HERSTEL: terug naar SCWindow - private var trackingArea: NSTrackingArea? - - init(windowCaptureManager: WindowCaptureManager, screen: NSScreen) { - self.windowCaptureManager = windowCaptureManager - self.screen = screen - super.init(frame: NSRect(origin: .zero, size: screen.frame.size)) - - self.wantsLayer = false - self.layerContentsRedrawPolicy = .onSetNeedsDisplay - self.autoresizingMask = [.width, .height] - self.translatesAutoresizingMaskIntoConstraints = true - - NSLog("๐ŸŽจโœ… VIEW INIT for screen: %@, Frame: %@, Bounds: %@", screen.customLocalizedName, NSStringFromRect(self.frame), NSStringFromRect(self.bounds)) - - setupTrackingArea() - - self.needsDisplay = true - DispatchQueue.main.async { - NSLog("๐ŸŽจโœ… VIEW INIT DELAYED DISPLAY for screen: %@", self.screen.customLocalizedName) - self.needsDisplay = true - self.displayIfNeeded() // Force display after setup - } - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func setupTrackingArea() { - // Remove existing tracking area - if let existingArea = trackingArea { - removeTrackingArea(existingArea) - } - - // Create new tracking area that covers this view - trackingArea = NSTrackingArea( - rect: bounds, - options: [.activeAlways, .mouseMoved, .inVisibleRect], - owner: self, - userInfo: nil - ) - - if let trackingArea = trackingArea { - addTrackingArea(trackingArea) - } - } - - override func updateTrackingAreas() { - super.updateTrackingAreas() - setupTrackingArea() - } - - override func draw(_ dirtyRect: NSRect) { - // NSLog("๐ŸŽจโœ… DRAWING ENTRY POINT for view on screen: %@ with dirtyRect: %@", self.window?.screen?.customLocalizedName ?? "UnknownScreen", NSStringFromRect(dirtyRect)) - super.draw(dirtyRect) - - NSColor.black.withAlphaComponent(0.05).setFill() - dirtyRect.fill() // Lichte overlay over het hele scherm - - // HERSTEL: Teken highlight voor hoveredWindow - guard let currentHoveredWindow = self.hoveredWindow, - let currentViewNSScreen = self.window?.screen, // Het NSScreen waar deze view op is - let mainScreenForConversion = self.windowCaptureManager.determinedMainScreen else { - // Als er geen window gehovered is, teken dan alleen de instructietekst op het hoofdscherm - if self.window?.screen == self.windowCaptureManager.determinedMainScreen { // Gebruik determinedMainScreen voor consistentie - drawInstructionText(on: self.window!.screen!, viewSize: self.bounds.size) - } - return - } - - let scWindowFrame = currentHoveredWindow.frame - // Converteer SCWindow.frame naar Globale AppKit coรถrdinaten (Y-omhoog, oorsprong linksonder hoofd NSScreen) - let globalHoveredWindowFrame = self.windowCaptureManager.convertSCWindowFrameToGlobal(scWindowFrame, mainScreen: mainScreenForConversion) - - // Converteer de globale frame naar coรถrdinaten lokaal aan *deze* view. - // De (0,0) van deze view is de linksonderhoek van het currentViewNSScreen. - let localHighlightX = globalHoveredWindowFrame.origin.x - currentViewNSScreen.frame.origin.x - let localHighlightY = globalHoveredWindowFrame.origin.y - currentViewNSScreen.frame.origin.y - let rectToHighlightInViewCoordinates = NSRect(x: localHighlightX, - y: localHighlightY, - width: globalHoveredWindowFrame.width, - height: globalHoveredWindowFrame.height) - - // NSLog("๐ŸŽจ DRAW on view for NSScreen \'%@\': Highlighting window \'%@\'", currentViewNSScreen.customLocalizedName, currentHoveredWindow.title ?? "N/A") - // NSLog("๐ŸŽจ SCWindow Frame: %@ (Layer: \(currentHoveredWindow.windowLayer))", NSStringFromRect(scWindowFrame)) - // NSLog("๐ŸŽจ Global Hovered Frame: %@", NSStringFromRect(globalHoveredWindowFrame)) - // NSLog("๐ŸŽจ Calculated Highlight Rect (local to view on screen \'%@\'): %@", currentViewNSScreen.customLocalizedName, NSStringFromRect(rectToHighlightInViewCoordinates)) - - // Teken alleen als de highlight rect daadwerkelijk de bounds van deze view snijdt. - // Dit is belangrijk omdat een venster over meerdere schermen kan spannen. - if self.bounds.intersects(rectToHighlightInViewCoordinates) { - NSColor.blue.withAlphaComponent(0.3).setFill() - // We clippen de highlight path naar de intersectie met de view bounds voor het geval het venster groter is dan dit schermsegment. - let drawingRect = self.bounds.intersection(rectToHighlightInViewCoordinates) - let highlightPath = NSBezierPath(rect: drawingRect) - highlightPath.fill() - - NSColor.blue.withAlphaComponent(0.8).setStroke() - highlightPath.lineWidth = 2 // Standaard lijndikte voor venster - highlightPath.stroke() - // NSLog("๐ŸŽจ Window highlight drawn (intersected rect: %@).", NSStringFromRect(drawingRect)) - } else { - // NSLog("๐ŸŽจ Highlight rect %@ does NOT intersect view bounds %@. Not drawing highlight on this screen segment.", NSStringFromRect(rectToHighlightInViewCoordinates), NSStringFromRect(self.bounds)) - } - - // Teken instructietekst (alleen op de primary overlay) - if currentViewNSScreen == self.windowCaptureManager.determinedMainScreen { // Gebruik determinedMainScreen - drawInstructionText(on: currentViewNSScreen, viewSize: self.bounds.size) - } - } - - private func drawInstructionText(on screen: NSScreen, viewSize: NSSize) { - // HERSTEL: Tekst voor window selectie - let instructionText = "Hover to select a window, Click to capture, ESC to cancel" - let attributes: [NSAttributedString.Key: Any] = [ - .font: NSFont.systemFont(ofSize: 18, weight: .medium), - .foregroundColor: NSColor.white.withAlphaComponent(0.9), - .strokeColor: NSColor.black.withAlphaComponent(0.5), - .strokeWidth: -2.0, - .paragraphStyle: { - let style = NSMutableParagraphStyle() - style.alignment = .center - return style - }() - ] - let attributedString = NSAttributedString(string: instructionText, attributes: attributes) - let textSize = attributedString.size() - - let textRect = NSRect(x: (viewSize.width - textSize.width) / 2, - y: viewSize.height - textSize.height - 30, - width: textSize.width, - height: textSize.height) - - let backgroundPadding: CGFloat = 10 - let backgroundRect = NSRect( - x: textRect.origin.x - backgroundPadding, - y: textRect.origin.y - backgroundPadding, - width: textRect.width + (2 * backgroundPadding), - height: textRect.height + (2 * backgroundPadding) - ) - let BORDER_RADIUS: CGFloat = 10 - let textBackgroundPath = NSBezierPath(roundedRect: backgroundRect, xRadius: BORDER_RADIUS, yRadius: BORDER_RADIUS) - NSColor.black.withAlphaComponent(0.4).setFill() - textBackgroundPath.fill() - - attributedString.draw(in: textRect) - // NSLog("๐ŸŽจ Instruction text drawn on main screen.") // Keep this one less verbose for now - } - - override func mouseMoved(with event: NSEvent) { - let globalLocation = NSEvent.mouseLocation - - var currentMouseNSScreen: NSScreen? - for s in NSScreen.screens { - if s.frame.contains(globalLocation) { - currentMouseNSScreen = s - break - } - } - - // Als de muis niet op het scherm van DEZE specifieke overlay view is, reset hoveredWindow voor DEZE view. - // Dit zorgt ervoor dat een venster dat over meerdere schermen spant, alleen gehighlight wordt op het scherm waar de muis is. - guard let mouseNSScreen = currentMouseNSScreen, mouseNSScreen.displayID == self.screen.displayID else { - if hoveredWindow != nil { - hoveredWindow = nil - needsDisplay = true - } - return - } - - // Muis is op het scherm van deze view. Kijk welk SCWindow (indien aanwezig) gehovered wordt. - let foundWindow = windowCaptureManager.findWindowAt(location: globalLocation) - - if hoveredWindow != foundWindow { // Vergelijk SCWindow objecten direct - hoveredWindow = foundWindow - needsDisplay = true - - // if foundWindow != nil { - // NSLog("๐Ÿ–ฑ๏ธ โœจ Hovered window SET to: \'%@\' (ID: \(foundWindow!.windowID), Layer: \(foundWindow!.windowLayer)) on screen %@", foundWindow!.title ?? "N/A", self.screen.customLocalizedName) - // } else { - // NSLog("๐Ÿ–ฑ๏ธ ๐Ÿ’จ No window hovered on screen %@ at %@", self.screen.customLocalizedName, NSStringFromPoint(globalLocation)) - // } - } - } - - // ๐Ÿ”ฅ NIEUW: Handle mouse clicks directly in the overlay view - override func mouseDown(with event: NSEvent) { - let globalLocation = NSEvent.mouseLocation - print("๐Ÿ”ฅ OVERLAY: Mouse click detected at \(globalLocation)") - - // Find window at click location - if let window = windowCaptureManager.findWindowAt(location: globalLocation) { - let selectedWindowTitle = window.title ?? "Untitled" - print("๐ŸŽฏ โœ… OVERLAY: Window selected: \(selectedWindowTitle) at \(globalLocation)") - - // Deactivate window selection mode first - windowCaptureManager.deactivateWindowSelectionMode() - - // Capture the window - Task { - print("๐Ÿ”ฅ OVERLAY: Starting window capture task for \(selectedWindowTitle)") - await windowCaptureManager.captureWindow(window) - print("๐Ÿ”ฅ OVERLAY: Window capture task completed for \(selectedWindowTitle)") - } - } else { - print("โŒ OVERLAY: No window found at \(globalLocation)") - print("๐Ÿ”ฅ DEBUG: Available windows count: \(windowCaptureManager.availableWindows.count)") - - // Extra debug: List all windows and their positions - for (index, win) in windowCaptureManager.availableWindows.enumerated() { - let frame = win.frame - let isPointInFrame = NSPointInRect(globalLocation, frame) - print("๐Ÿ”ฅ DEBUG: Window \(index): \(win.title ?? "Untitled") - Frame: \(frame) - Contains point: \(isPointInFrame)") - } - - windowCaptureManager.deactivateWindowSelectionMode() - } - } - - override func keyDown(with event: NSEvent) { - if event.keyCode == 53 { // ESC - windowCaptureManager.deactivateWindowSelectionMode() - } - } - - override var acceptsFirstResponder: Bool { - return true - } -} - -// MARK: - ScreenshotApp Extension for Window Capture Integration -extension ScreenshotApp { - - // Add window capture manager property to ScreenshotApp - private static var windowCaptureManagerKey: UInt8 = 0 - - var windowCaptureManager: WindowCaptureManager? { - get { - return objc_getAssociatedObject(self, &ScreenshotApp.windowCaptureManagerKey) as? WindowCaptureManager - } - set { - objc_setAssociatedObject(self, &ScreenshotApp.windowCaptureManagerKey, newValue, .OBJC_ASSOCIATION_RETAIN) - } - } - - func initializeWindowCaptureManager() { - if #available(macOS 12.3, *) { - windowCaptureManager = WindowCaptureManager(screenshotApp: self) - print("โœ… WindowCaptureManager initialized") - } else { - print("โš ๏ธ WindowCaptureManager requires macOS 12.3 or later") - } - } - - func processWindowCapture(image: NSImage, windowTitle: String?) { - // Process window capture similar to regular screenshot capture - print("๐ŸชŸ Processing window capture: \(windowTitle ?? "Untitled Window")") - - let tempURL = createTempUrl() - setTempFileURL(tempURL) - - guard let tiffData = image.tiffRepresentation, - let bitmap = NSBitmapImageRep(data: tiffData), - let pngData = bitmap.representation(using: .png, properties: [:]) else { - print("โŒ Failed to convert window capture to PNG") - return - } - - do { - try pngData.write(to: tempURL) - print("๐Ÿ’พ Window capture saved temporarily: \(tempURL.path)") - - // Play sound if enabled - if SettingsManager.shared.playSoundOnCapture { - NSSound(named: "Glass")?.play() - } - - // Show preview with window title context - showPreview(image: image, windowTitle: windowTitle) - } catch { - print("โŒ Failed to save window capture: \(error)") - } - } - - private func showPreview(image: NSImage, windowTitle: String?) { - // Use existing preview logic but potentially add window title to UI - showPreview(image: image) - - // You could extend the preview UI to show window title - if let title = windowTitle { - print("๐Ÿ“ธ Window captured: \(title)") - } - } -} \ No newline at end of file diff --git a/ShotScreen/Sources/images/BannerFinder.png b/ShotScreen/Sources/images/BannerFinder.png deleted file mode 100644 index 5acc2d9..0000000 Binary files a/ShotScreen/Sources/images/BannerFinder.png and /dev/null differ diff --git a/ShotScreen/Sources/images/FinderBackground.png b/ShotScreen/Sources/images/FinderBackground.png deleted file mode 100644 index 941ae50..0000000 Binary files a/ShotScreen/Sources/images/FinderBackground.png and /dev/null differ diff --git a/ShotScreen/Sources/images/MenuIcon.png b/ShotScreen/Sources/images/MenuIcon.png deleted file mode 100644 index 2a9e17f..0000000 Binary files a/ShotScreen/Sources/images/MenuIcon.png and /dev/null differ diff --git a/ShotScreen/Sources/images/ShotScreenIcon.png b/ShotScreen/Sources/images/ShotScreenIcon.png deleted file mode 100644 index 6bb282d..0000000 Binary files a/ShotScreen/Sources/images/ShotScreenIcon.png and /dev/null differ diff --git a/ShotScreen/Sources/images/ShotScreenIcon_200x200.png b/ShotScreen/Sources/images/ShotScreenIcon_200x200.png deleted file mode 100644 index 86ef4e6..0000000 Binary files a/ShotScreen/Sources/images/ShotScreenIcon_200x200.png and /dev/null differ diff --git a/ShotScreen/Sources/images/ShotScreenIcon_600x600.png b/ShotScreen/Sources/images/ShotScreenIcon_600x600.png deleted file mode 100644 index 961ef35..0000000 Binary files a/ShotScreen/Sources/images/ShotScreenIcon_600x600.png and /dev/null differ diff --git a/ShotScreen/Sources/images/ShotScreenIcon_600x600_background_colour.png b/ShotScreen/Sources/images/ShotScreenIcon_600x600_background_colour.png deleted file mode 100644 index 39a5481..0000000 Binary files a/ShotScreen/Sources/images/ShotScreenIcon_600x600_background_colour.png and /dev/null differ diff --git a/ShotScreen/Sources/images/ShotScreen_Banner.png b/ShotScreen/Sources/images/ShotScreen_Banner.png deleted file mode 100644 index 10ce3c6..0000000 Binary files a/ShotScreen/Sources/images/ShotScreen_Banner.png and /dev/null differ diff --git a/ShotScreen/Sources/images/Wizard_TurnOffSceenShot.png b/ShotScreen/Sources/images/Wizard_TurnOffSceenShot.png deleted file mode 100644 index 4b3eff4..0000000 Binary files a/ShotScreen/Sources/images/Wizard_TurnOffSceenShot.png and /dev/null differ diff --git a/ShotScreen/Sources/main.swift b/ShotScreen/Sources/main.swift deleted file mode 100644 index 9fc2da4..0000000 --- a/ShotScreen/Sources/main.swift +++ /dev/null @@ -1,3679 +0,0 @@ -import AppKit -import HotKey -import Sparkle -import UniformTypeIdentifiers -import SwiftUI -import ServiceManagement -import UserNotifications - -// Grid-gerelateerde klassen en protocol -protocol GridViewManagerDelegate: AnyObject { - func gridViewManager(_ manager: GridViewManager, didDropImage imageURL: URL, ontoCell cellIndex: Int, at dropPoint: NSPoint) - func getActiveWindowForGridPositioning() -> NSWindow? // Nodig voor positionering GridWindow -} - -// Make ScreenshotApp conform to DraggableImageViewClickHandler -class ScreenshotApp: NSObject, NSApplicationDelegate, RenameActionHandlerDelegate, GridViewManagerDelegate, DraggableImageViewClickHandler, MenuManagerDelegate, PreviewManagerDelegate, GridActionManagerDelegate { - var hotKey: HotKey! - var lastImage: NSImage? - - // ๐ŸŽฏ NEW: Thumbnail restoration backup system for thumbnail restoration after settings apply - private var backupImagePath: String? - private var backupImage: NSImage? - - // NIEUW: Runtime Cmd monitoring voor all screens toggle - - var activePreviewWindow: NSWindow? // Het standaard screenshot preview venster - private var currentImageView: DraggableImageView? - var tempURL: URL? - private var isClosing = false - private var menuManager: MenuManager! // NIEUW: MenuManager instance - private var previewManager: PreviewManager! // NIEUW: PreviewManager instance - private var gridActionManager: GridActionManager! // NIEUW: GridActionManager instance - var gridViewManager: GridViewManager? - var didGridHandleDrop = false // Flag om dubbele events te voorkomen - - // ๐Ÿ”„ NEW: Flag voor stash grid action handling - var didStashGridHandleDrop = false - private var isPreviewUpdating = false - private var previewDismissTimer: Timer? - var renameActionHandler: RenameActionHandler! - var activeOverlayWindow: OverlayWindow? - private var previewWasVisibleBeforeSettingsOpen: Bool = false - let settings = SettingsManager.shared // <-- TOEGEVOEGD: SettingsManager instance - - // ๐Ÿงช NEW: Cache cleanup timer for dynamic scheduling - private var cacheCleanupTimer: Timer? - - // NIEUW: Provider voor ScreenCaptureKit (Intel Mac compatible) - let screenCaptureProvider: ScreenCaptureKitProvider? = { - if #available(macOS 12.3, *) { - return ScreenCaptureKitProvider() - } else { - print("โš ๏ธ ScreenCaptureKit requires macOS 12.3 or later (Intel Mac fallback)") - return nil - } - }() - - // Houd de window controller en de image store voor het Stash venster bij - private var stashWindowController: NSWindowController? - var activeStashImageStore: GalleryImageStore? - - // ๐Ÿ”ฅ NIEUW: Persistent stash image store voor persistent images - private var persistentStashImageStore: GalleryImageStore? - - // ๐Ÿ”„ UPDATE: Update Manager for Sparkle functionality - private let updateManager = UpdateManager.shared - private var activeSettingsWindow: SettingsWindow? // NIEUW: Referentie naar open instellingenvenster - - private let launchHelperBundleID = "com.shotscreen.launchhelper" // Definieer de Bundle ID van de helper - private var firstLaunchWizard: FirstLaunchWizard? // NIEUW: First launch wizard window - - // NIEUW: FinderWindowManager voor het bewaren en herstellen van Finder vensters - private var finderWindowManager: FinderWindowManager! - - // NIEUW: Thumbnail directory voor permanente screenshot opslag - private lazy var thumbnailDirectory: URL = { - let appSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! - let shotScreenDirectory = appSupportDirectory.appendingPathComponent("ShotScreen") - let thumbnailsDirectory = shotScreenDirectory.appendingPathComponent("Thumbnails") - - // Maak de directory aan als die niet bestaat - try? FileManager.default.createDirectory(at: thumbnailsDirectory, withIntermediateDirectories: true, attributes: nil) - - // ๐ŸŽฏ NEW: Ensure thumbnail restoration backup directory exists - let restorationDirectory = thumbnailsDirectory.appendingPathComponent("thumbnail_restoration") - try? FileManager.default.createDirectory(at: restorationDirectory, withIntermediateDirectories: true, attributes: nil) - - return thumbnailsDirectory - }() - - - // MARK: - Multi-Monitor Support Properties - var overlayWindows: [NSWindow] = [] - var windowScreenMap: [NSWindow: NSScreen] = [:] - var isDragging = false - var startPoint: NSPoint = NSZeroPoint - var currentEndPoint: NSPoint = NSZeroPoint - var globalMouseDownMonitor: Any? - var globalMouseDragMonitor: Any? - var globalMouseUpMonitor: Any? - var localMouseDownMonitor: Any? - var localMouseDragMonitor: Any? - var localMouseUpMonitor: Any? - - // MARK: - Multi-Monitor State - var isMultiMonitorSelectionActive = false - var isAllScreensCaptureToggledOn = false // NIEUW: State voor getogglede "alle schermen" modus - var lastKnownModifierFlags: NSEvent.ModifierFlags = NSEvent.ModifierFlags() // Nieuwe property voor Command-toggle - - // MARK: - Event Capture Overlay - var eventCaptureWindow: NSWindow? - - // MARK: - Performance Optimization - var lastUpdateTime: CFTimeInterval = 0 - let updateThrottle: CFTimeInterval = 1.0 / 120.0 // 120 FPS max - - // MARK: - Mouse Click Detection - var mouseDownLocation: NSPoint = NSZeroPoint - var mouseDownTime: TimeInterval = 0 - var hasMouseMoved = false - var isAllScreenModifierPressed = false - - // MARK: - Additional Multi-Monitor Properties - var globalKeyMonitor: Any? - var globalMouseMonitor: Any? - var crosshairWindows: [NSWindow] = [] - var crosshairTrackingMonitor: Any? - var eventCaptureWindows: [NSWindow] = [] - - // Reset tracking variables for clean state - func resetTrackingVariables() { - mouseDownTime = 0 - mouseDownLocation = NSZeroPoint - hasMouseMoved = false - isAllScreenModifierPressed = false - print("๐Ÿ”„ Tracking variables reset for clean state") - } - - func applicationDidFinishLaunching(_ notification: Notification) { - // ๐Ÿ”ง DEBUG: Check for command line arguments - let arguments = CommandLine.arguments - if arguments.contains("--force-debug-update") { - print("๐Ÿ”ง DEBUG: Force debug update mode detected") - performForceDebugUpdate() - return // Don't continue with normal app initialization - } - - hotKey = HotKey(key: .four, modifiers: [.command, .shift]) - hotKey.keyDownHandler = { [weak self] in - print("๐Ÿ”ฅ Hotkey pressed - launching native macOS screencapture") - self?.activateMultiMonitorSelection() - } - - renameActionHandler = RenameActionHandler(delegate: self) - - // NIEUW: Initialize MenuManager - menuManager = MenuManager(delegate: self) - menuManager.setupMenu() - - // NIEUW: Initialize PreviewManager - previewManager = PreviewManager(delegate: self) - - // NIEUW: Initialize GridActionManager - gridActionManager = GridActionManager(delegate: self) - - // NIEUW: Initialize FinderWindowManager - finderWindowManager = FinderWindowManager() - - // NIEUW: Initialize WindowCaptureManager - initializeWindowCaptureManager() - - gridViewManager = GridViewManager() - gridViewManager?.delegate = self - - // NIEUW: Use cache management system for thumbnail cleanup instead of fixed count limit - // cleanupOldThumbnails() - REMOVED: This limited to 10 files, use cache retention instead - - // MARK: - Multi-Monitor Setup - print("๐Ÿš€ Screenshot System Started with Double Hotkey Detection!") - print("๐Ÿ“ Press Cmd+Shift+4 for normal screenshot selection") - print(" ๐ŸŽฏ Press Cmd+Shift+4 TWICE (within 1 second) = capture whole screen under cursor") - print(" ๐ŸŽฏ Single press = normal selection capture") - print(" ๐ŸชŸ Spatiebalk during drag = window capture") - print(" ๐Ÿšซ ESC = cancel (no thumbnail!)") - print("๐ŸŽฏ Double hotkey = instant whole screen capture!") - - // List available screens - listAvailableScreens() - - // Setup global mouse tracking for multi-monitor support - setupGlobalMouseTracking() - - // Registreer voor setting change notificaties - registerForSettingsChanges() - - - - // Pas opstarten bij login toe op basis van instelling - toggleLaunchAtLogin(shouldLaunch: SettingsManager.shared.startAppOnLogin) - // Observeer wijzigingen in de opstartinstelling - NotificationCenter.default.addObserver(self, selector: #selector(handleStartAppOnLoginChanged), name: .startAppOnLoginSettingChanged, object: nil) - - // NIEUW: Add observers for grid actions - NotificationCenter.default.addObserver(self, selector: #selector(handleGridCancelAction), name: .gridActionCancelRequested, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(handleGridRemoveAction), name: .gridActionRemoveRequested, object: nil) - - // NIEUW: Check for first launch and show wizard - checkAndShowFirstLaunchWizard() - - // NIEUW: Setup initial hotkey based on settings - setupHotKey() - - // ๐Ÿ”„ NIEUW: Initialize automatic updates for direct sales - setupAutomaticUpdates() - - // ๐Ÿ—‚๏ธ NIEUW: Perform cache cleanup on startup - performStartupCacheCleanup() - - // ๐Ÿ” LICENSE: Initialize license system - initializeLicenseSystem() - } - - // VERWIJDERD: cleanupOldThumbnails() - Deze functie beperkte thumbnails tot 10 bestanden - // Nu gebruikt de app het cache retention systeem dat gebaseerd is op tijd (bijv. 1 uur) - // Alle thumbnails blijven bewaard totdat de ingestelde retention tijd verstreken is - - func registerForSettingsChanges() { - NotificationCenter.default.addObserver(self, selector: #selector(handleCloseAfterDragChanged), name: .closeAfterDragSettingChanged, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(handleCloseAfterSaveChanged), name: .closeAfterSaveSettingChanged, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(handlePlaySoundOnCaptureChanged), name: .playSoundOnCaptureSettingChanged, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(handleStashAlwaysOnTopChanged), name: .stashAlwaysOnTopSettingChanged, object: nil) - // ๐Ÿ”ฅ NIEUW: Observer for persistent stash setting changes - NotificationCenter.default.addObserver(self, selector: #selector(handlePersistentStashChanged), name: .persistentStashChanged, object: nil) - // NIEUW: Observer for shortcut changes - NotificationCenter.default.addObserver(self, selector: #selector(handleShortcutSettingChanged), name: .shortcutSettingChanged, object: nil) - // NIEUW: Observer for desktop icons setting changes - NotificationCenter.default.addObserver(self, selector: #selector(handleHideDesktopIconsChanged), name: .hideDesktopIconsSettingChanged, object: nil) - // ๐Ÿงช NIEUW: Observer for cache retention setting changes - NotificationCenter.default.addObserver(self, selector: #selector(handleCacheRetentionTimeChanged), name: NSNotification.Name("cacheRetentionTimeChanged"), object: nil) - // Voeg hier observers toe voor andere settings die directe actie in ScreenshotApp vereisen - } - - @objc func handleCloseAfterDragChanged() { - print("Setting changed: closeAfterDrag is now \(SettingsManager.shared.closeAfterDrag)") - // Geen directe actie nodig hier, de waarde wordt gelezen wanneer nodig. - // Als dit de UI van de preview zou beรฏnvloeden, roep hier updatePreviewSize() aan. - } - - @objc func handleCloseAfterSaveChanged() { - print("Setting changed: closeAfterSave is now \(SettingsManager.shared.closeAfterSave)") - // Geen directe actie nodig hier. - } - - @objc func handlePlaySoundOnCaptureChanged() { - print("Setting changed: playSoundOnCapture is now \(SettingsManager.shared.playSoundOnCapture)") - // Geen directe actie nodig hier, capture() leest de waarde. - } - - // NIEUW: Handler voor wijziging opstartinstelling - @objc func handleStartAppOnLoginChanged() { - print("Setting changed: startAppOnLogin is now \(SettingsManager.shared.startAppOnLogin)") - toggleLaunchAtLogin(shouldLaunch: SettingsManager.shared.startAppOnLogin) - } - - // NIEUW: Handler voor wijziging stash always on top instelling - @objc func handleStashAlwaysOnTopChanged() { - print("Setting changed: stashAlwaysOnTop is now \(SettingsManager.shared.stashAlwaysOnTop)") - updateStashWindowLevel() - } - - // ๐Ÿ”ฅ NIEUW: Handler voor wijziging persistent stash instelling - @objc func handlePersistentStashChanged() { - print("๐Ÿ”ฅ PERSISTENT STASH: Setting changed - persistentStash is now \(SettingsManager.shared.persistentStash)") - - if !SettingsManager.shared.persistentStash { - print("๐Ÿ”ฅ PERSISTENT STASH: Feature disabled - clearing persistent image store") - persistentStashImageStore = nil - } - } - - // NIEUW: Handler voor wijziging shortcut instelling - @objc func handleShortcutSettingChanged() { - print("Setting changed: shortcut configuration updated") - setupHotKey() - } - - @objc func handleHideDesktopIconsChanged() { - print("Setting changed: hideDesktopIconsDuringScreenshot is now \(SettingsManager.shared.hideDesktopIconsDuringScreenshot)") - // Update the menu item to reflect the new state - menuManager?.refreshDesktopIconsMenuItem() - } - - // ๐Ÿงช NIEUW: Handler for cache retention time changes - @objc func handleCacheRetentionTimeChanged() { - let retentionTime = SettingsManager.shared.cacheRetentionTime - print("๐Ÿงช CACHE: Setting changed - cacheRetentionTime is now \(retentionTime.displayName)") - - // Trigger immediate cleanup to test new retention time - print("๐Ÿงช CACHE: Triggering immediate cleanup for testing...") - DispatchQueue.global(qos: .utility).async { - CacheManager.shared.cleanupOldCache() - } - - // Reschedule periodic cleanup with new interval - reschedulePeriodicCacheCleanup() - } - - // NIEUW: Setup hotkey based on current settings - private func setupHotKey() { - // Remove existing hotkeys - hotKey = nil - - let settings = SettingsManager.shared - - if settings.useCustomShortcut && settings.customShortcutModifiers != 0 && settings.customShortcutKey != 0 { - // Use custom shortcut - let modifiers = convertToHotKeyModifiers(settings.customShortcutModifiers) - if let key = convertToHotKeyKey(settings.customShortcutKey) { - hotKey = HotKey(key: key, modifiers: modifiers) - hotKey.keyDownHandler = { [weak self] in - print("๐Ÿ”ฅ Custom hotkey pressed - activating multi-monitor selection mode") - self?.activateMultiMonitorSelection() - } - print("๐ŸŽฏ Custom hotkey set: \(formatShortcutForLog(modifiers: settings.customShortcutModifiers, keyCode: settings.customShortcutKey))") - } else { - print("โŒ Invalid custom shortcut key, falling back to default") - setupDefaultHotKey() - } - } else { - // Use default Cmd+Shift+4 - setupDefaultHotKey() - } - } - - private func setupDefaultHotKey() { - hotKey = HotKey(key: .four, modifiers: [.command, .shift]) - hotKey.keyDownHandler = { [weak self] in - print("๐Ÿ”ฅ Default hotkey pressed - activating native screencapture mode") - self?.activateMultiMonitorSelection() - } - print("๐ŸŽฏ Default hotkey set: Cmd+Shift+4 (native screencapture)") - } - - // activateWindowCaptureMode function removed - use native spatiebalk instead - - private func convertToHotKeyModifiers(_ modifiers: UInt) -> NSEvent.ModifierFlags { - var result: NSEvent.ModifierFlags = [] - if modifiers & (1 << 0) != 0 { result.insert(.command) } - if modifiers & (1 << 1) != 0 { result.insert(.shift) } - if modifiers & (1 << 2) != 0 { result.insert(.option) } - if modifiers & (1 << 3) != 0 { result.insert(.control) } - return result - } - - private func convertToHotKeyKey(_ keyCode: UInt16) -> Key? { - // Map key codes to HotKey.Key enum values - switch keyCode { - case 0: return .a - case 1: return .s - case 2: return .d - case 3: return .f - case 4: return .h - case 5: return .g - case 6: return .z - case 7: return .x - case 8: return .c - case 9: return .v - case 11: return .b - case 12: return .q - case 13: return .w - case 14: return .e - case 15: return .r - case 16: return .y - case 17: return .t - case 18: return .one - case 19: return .two - case 20: return .three - case 21: return .four - case 22: return .six - case 23: return .five - case 25: return .nine - case 26: return .seven - case 28: return .eight - case 29: return .zero - case 31: return .o - case 32: return .u - case 34: return .i - case 35: return .p - case 37: return .l - case 38: return .j - case 40: return .k - case 45: return .n - case 46: return .m - case 36: return .return - case 48: return .tab - case 49: return .space - case 51: return .delete - case 53: return .escape - case 122: return .f1 - case 120: return .f2 - case 99: return .f3 - case 118: return .f4 - case 96: return .f5 - case 97: return .f6 - case 98: return .f7 - case 100: return .f8 - case 101: return .f9 - case 109: return .f10 - case 103: return .f11 - case 111: return .f12 - default: return nil - } - } - - private func formatShortcutForLog(modifiers: UInt, keyCode: UInt16) -> String { - var parts: [String] = [] - - if modifiers & (1 << 3) != 0 { parts.append("Ctrl") } - if modifiers & (1 << 2) != 0 { parts.append("Opt") } - if modifiers & (1 << 1) != 0 { parts.append("Shift") } - if modifiers & (1 << 0) != 0 { parts.append("Cmd") } - - if let key = convertToHotKeyKey(keyCode) { - parts.append("\(key)") - } else { - parts.append("Key\(keyCode)") - } - - return parts.joined(separator: "+") - } - - // NIEUW: Grid action handlers - @objc func handleGridCancelAction() { - print("๐Ÿšซ Grid cancel action received via notification") - // Legacy: This is now handled by GridActionManager directly - gridViewManager?.hideGrid(monitorForReappear: false) - } - - @objc func handleGridRemoveAction() { - print("๐Ÿ—‘ Grid remove action received via notification") - // Legacy: This is now handled by GridActionManager directly - gridViewManager?.hideGrid(monitorForReappear: false) - if let temp = self.tempURL { - print("๐Ÿ—‘๏ธ Removing temp file: \(temp.path)") - try? FileManager.default.removeItem(at: temp) - self.setTempFileURL(nil) - } - closePreviewWithAnimation(immediate: true) - } - - // NIEUW: First Launch Wizard - private func checkAndShowFirstLaunchWizard() { - if !SettingsManager.shared.hasCompletedFirstLaunch { - // Show the wizard - firstLaunchWizard = FirstLaunchWizard() - firstLaunchWizard?.makeKeyAndOrderFront(nil as Any?) - // Optionally, bring to front - firstLaunchWizard?.orderFrontRegardless() - print("๐Ÿ‘‹ First launch wizard shown") - } else { - print("๐Ÿ‘‹ Welcome back! First launch already completed") - } - } - - // Function to capture the current screen based on mouse location - func captureCurrentScreen(at clickLocation: NSPoint) { - guard let screen = NSScreen.screens.first(where: { $0.frame.contains(clickLocation) }) else { - NSLog("Error: Could not determine screen for single click at \(clickLocation)") - return - } - NSLog("๐ŸŽฏ Capturing full screen: \(screen.customLocalizedName) due to single click.") - - Task { [weak self] in - guard let self = self else { return } - // Intel Mac compatible ScreenCaptureKit usage - if let provider = self.screenCaptureProvider { - let windowsToExclude = await provider.getAllWindowsToExclude() - if let image = await provider.captureScreen(screen: screen, excludingWindows: windowsToExclude) { - await MainActor.run { - self.processCapture(image: image) - } - } else { - NSLog("Error: Failed to capture full screen \(screen.customLocalizedName) using ScreenCaptureKitProvider") - } - } else { - // Fallback to native screencapture for Intel Macs without ScreenCaptureKit - print("โšก Intel Mac fallback: Using native screencapture for full screen capture") - self.captureCurrentScreenNative(screen: screen) - } - } - } - - // NIEUW: Intel Mac fallback for native screencapture of specific screen - private func captureCurrentScreenNative(screen: NSScreen) { - guard let screenNumber = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber else { - print("โŒ Could not get screen number for native capture") - return - } - - // Create temporary file for screenshot - let tempDirectory = FileManager.default.temporaryDirectory - let tempFileName = "ShotScreen_intel_\(UUID().uuidString).png" - let tempFileURL = tempDirectory.appendingPathComponent(tempFileName) - - print("๐Ÿ“„ Intel Mac using temporary file: \(tempFileURL.path)") - - // Native screencapture for specific display (NO clipboard) - let task = Process() - task.launchPath = "/usr/sbin/screencapture" - task.arguments = ["-D", "\(screenNumber.intValue)", "-x", tempFileURL.path] // specific display to file, no sounds - - do { - try task.run() - - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - task.waitUntilExit() - - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - print("โœ… Intel Mac: Native screencapture completed for \(screen.customLocalizedName)") - - // Check if file was created and process it - if FileManager.default.fileExists(atPath: tempFileURL.path) { - self.processFileImage(at: tempFileURL) - } else { - print("โŒ Intel Mac: No screenshot file created") - } - } - } - } catch { - print("โŒ Intel Mac: Failed to start native screencapture: \(error)") - } - } - - // Function to capture all screens - // ๐Ÿ”ฅ NIEUW: Window capture at specific location - func captureWindowAt(location: NSPoint) { - if #available(macOS 12.3, *) { - if let windowManager = windowCaptureManager { - Task { - await windowManager.captureWindowAt(point: location) - } - } else { - print("โŒ WindowCaptureManager not available for window capture") - } - } else { - print("โš ๏ธ Window capture requires macOS 12.3 or later") - } - } - - // ๐Ÿ”„ UPDATE: Setup automatic updates using UpdateManager - @MainActor private func setupAutomaticUpdates() { - print("๐Ÿ”„ UPDATE: Setting up UpdateManager...") - updateManager.delegate = self - updateManager.printDebugInfo() - } - - // ๐Ÿ—‚๏ธ NEW: Perform cache cleanup on startup - private func performStartupCacheCleanup() { - print("๐Ÿ—‚๏ธ CACHE: Performing startup cache cleanup...") - - // Perform cleanup in background to avoid blocking startup - DispatchQueue.global(qos: .utility).async { - CacheManager.shared.cleanupOldCache() - } - - // Schedule periodic cleanup with dynamic interval - schedulePeriodicCacheCleanup() - } - - // ๐Ÿ—‚๏ธ NEW: Schedule periodic cache cleanup with dynamic interval based on retention time - private func schedulePeriodicCacheCleanup() { - let retentionTime = SettingsManager.shared.cacheRetentionTime - let cleanupInterval = getCleanupInterval(for: retentionTime) - - cacheCleanupTimer = Timer.scheduledTimer(withTimeInterval: cleanupInterval, repeats: true) { _ in - print("๐Ÿ—‚๏ธ CACHE: Performing scheduled cache cleanup...") - DispatchQueue.global(qos: .utility).async { - CacheManager.shared.cleanupOldCache() - } - } - print("๐Ÿ—‚๏ธ CACHE: Scheduled periodic cleanup every \(Int(cleanupInterval)) seconds for retention: \(retentionTime.displayName)") - } - - // ๐Ÿงช NEW: Reschedule periodic cache cleanup with new settings - private func reschedulePeriodicCacheCleanup() { - // Cancel existing timer - cacheCleanupTimer?.invalidate() - cacheCleanupTimer = nil - - // Schedule new timer with updated interval - schedulePeriodicCacheCleanup() - } - - // ๐Ÿงช NEW: Get cleanup interval based on retention time - private func getCleanupInterval(for retentionTime: CacheRetentionTime) -> TimeInterval { - switch retentionTime { - case .oneHour: - return 300 // Check every 5 minutes for 1 hour retention - case .sixHours, .twelveHours: - return 1800 // Check every 30 minutes - case .oneDay, .threeDays: - return 3600 // Check every hour - case .oneWeek, .twoWeeks, .oneMonth: - return 7200 // Check every 2 hours - case .forever: - return 86400 // Check once per day (in case setting changes) - } - } - - // ๐Ÿ”„ UPDATE: Manual update check via UpdateManager - @MainActor func checkForUpdates() { - print("๐Ÿ” UPDATE: Triggering manual update check...") - updateManager.checkForUpdates() - } - - // MARK: - Dynamic About Dialog Helper - private func getDynamicAboutDialogTitle() -> String { - let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" - let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" - return "๐ŸŽฏ About ShotScreen v\(version) (\(build)) ULTIMATE!" - } - - @MainActor func showAbout() { - print("โ„น๏ธ ABOUT: Showing about dialog...") - - let alert = NSAlert() - alert.messageText = getDynamicAboutDialogTitle() - - let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Unknown" - let buildNumber = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "Unknown" - - alert.informativeText = """ - Version: \(version) (Build \(buildNumber)) - - ShotScreen is a powerful screenshot utility for macOS. - - Features: - โ€ข Screenshot capture with custom regions - โ€ข Window capture - โ€ข All screens capture - โ€ข Stash for organizing screenshots - โ€ข Automatic updates via Sparkle - - ยฉ 2025 Your Company Name. All rights reserved. - """ - - alert.addButton(withTitle: "OK") - alert.addButton(withTitle: "Check for Updates") - alert.alertStyle = .informational - - // Set app icon if available - if let appIcon = NSApplication.shared.applicationIconImage { - alert.icon = appIcon - } - - let response = alert.runModal() - if response == .alertSecondButtonReturn { - // User clicked "Check for Updates" - checkForUpdates() - } - } - - func captureAllScreens() { - NSLog("๐ŸŽฏ Capturing all screens.") - - // Reset de toggle na het capturen - if isAllScreensCaptureToggledOn { - isAllScreensCaptureToggledOn = false - updateAllScreensModeNotifier() // Notifier ook updaten - print("๐Ÿ”„ All Screens Capture Toggled Off after capture.") - } - - // ๐ŸŽฏ Check of desktop filtering nodig is - let needsDesktopFiltering = SettingsManager.shared.hideDesktopIconsDuringScreenshot || - SettingsManager.shared.hideDesktopWidgetsDuringScreenshot - - if needsDesktopFiltering { - // Gebruik ScreenCaptureKit voor filtering - print("๐ŸŽฏ Using ScreenCaptureKit for all screens capture (desktop filtering enabled)") - captureAllScreensWithFiltering() - } else { - // Gebruik native screencapture voor alle schermen - print("๐ŸŽฏ Using native screencapture for all screens capture") - captureAllScreensNative() - } - } - - // ๐ŸŽฏ BESTAANDE FUNCTIE: ScreenCaptureKit all screens capture - private func captureAllScreensWithFiltering() { - // Toon loading indicator voor stitching process - previewManager.showLoadingIndicator() - - Task { [weak self] in - guard let self = self else { return } - - // Intel Mac compatible ScreenCaptureKit usage - guard let provider = self.screenCaptureProvider else { - print("โšก Intel Mac fallback: ScreenCaptureKit not available, using native all screens capture") - await MainActor.run { - self.previewManager.hideLoadingIndicator() - self.captureAllScreensNative() - } - return - } - - let windowsToExclude = await provider.getAllWindowsToExclude() - - let allScreens = NSScreen.screens - guard !allScreens.isEmpty else { - NSLog("Error: No screens found to capture.") - await MainActor.run { - self.previewManager.hideLoadingIndicator() - } - return - } - - var totalBounds = CGRect.null - for screen in allScreens { - totalBounds = totalBounds.union(screen.frame) - } - NSLog("๐Ÿ–ผ๏ธ Total combined bounds for all screens: \(totalBounds)") - - var imagePartsToDraw: [(image: NSImage, rect: NSRect, screenName: String)] = [] - var allCapturedSuccessfully = true - - for screen in allScreens { - NSLog("๐Ÿ“ธ Attempting to capture screen for combined image: \(screen.customLocalizedName)") - if let imagePart = await provider.captureScreen(screen: screen, excludingWindows: windowsToExclude) { - let drawRectX = screen.frame.origin.x - totalBounds.origin.x - let drawRectY = screen.frame.origin.y - totalBounds.origin.y - let targetRect = NSRect(x: drawRectX, y: drawRectY, width: screen.frame.width, height: screen.frame.height) - imagePartsToDraw.append((image: imagePart, rect: targetRect, screenName: screen.customLocalizedName)) - } else { - NSLog("Error: Failed to capture screen \(screen.customLocalizedName) for combined image.") - allCapturedSuccessfully = false - // Optioneel: break om te stoppen bij de eerste fout - } - } - - if allCapturedSuccessfully && !imagePartsToDraw.isEmpty { - await MainActor.run { - let combinedImage = NSImage(size: totalBounds.size) - combinedImage.lockFocus() - for item in imagePartsToDraw { - item.image.draw(in: item.rect) - NSLog("๐Ÿ–ผ Drawn screen \(item.screenName) at \(item.rect) on combined image") - } - combinedImage.unlockFocus() - - // Verberg loading indicator voordat we de preview tonen - self.previewManager.hideLoadingIndicator() - self.processCapture(image: combinedImage) - } - } else if !allCapturedSuccessfully { - await MainActor.run { - NSLog("Error: Not all screens were captured successfully for the combined image.") - self.previewManager.hideLoadingIndicator() - } - } else { - await MainActor.run { - NSLog("Error: No images were captured to create a combined image.") - self.previewManager.hideLoadingIndicator() - } - } - } - } - - // ๐Ÿ”ง OUDE WERKENDE FUNCTIE: Screen-by-screen capture en combinatie (restored from working version) - private func captureAllScreensNative() { - print("๐Ÿ“ธ NATIVE all screens capture starting (using ScreenCaptureKit method)...") - print("๐Ÿ”ง BYPASSING desktop filtering for multi-click all screens") - - // ๐Ÿ”ง CRITICAL FIX: Deactivate multi-monitor selection mode IMMEDIATELY - deactivateMultiMonitorSelection() - - // Toon loading indicator voor stitching process - previewManager.showLoadingIndicator() - - Task { [weak self] in - guard let self = self else { return } - let windowsToExclude = await self.screenCaptureProvider?.getAllWindowsToExclude() ?? [] - - let allScreens = NSScreen.screens - guard !allScreens.isEmpty else { - print("โŒ Error: No screens found to capture.") - await MainActor.run { - self.previewManager.hideLoadingIndicator() - } - return - } - - var totalBounds = CGRect.null - for screen in allScreens { - totalBounds = totalBounds.union(screen.frame) - } - print("๐Ÿ–ผ๏ธ Total combined bounds for all screens: \(totalBounds)") - - var imagePartsToDraw: [(image: NSImage, rect: NSRect, screenName: String)] = [] - var allCapturedSuccessfully = true - - for screen in allScreens { - print("๐Ÿ“ธ Attempting to capture screen for combined image: \(screen.customLocalizedName)") - if let imagePart = await self.screenCaptureProvider?.captureScreen(screen: screen, excludingWindows: windowsToExclude) { - let drawRectX = screen.frame.origin.x - totalBounds.origin.x - let drawRectY = screen.frame.origin.y - totalBounds.origin.y - let targetRect = NSRect(x: drawRectX, y: drawRectY, width: screen.frame.width, height: screen.frame.height) - imagePartsToDraw.append((image: imagePart, rect: targetRect, screenName: screen.customLocalizedName)) - } else { - print("โŒ Error: Failed to capture screen \(screen.customLocalizedName) for combined image.") - allCapturedSuccessfully = false - // Optioneel: break om te stoppen bij de eerste fout - } - } - - if allCapturedSuccessfully && !imagePartsToDraw.isEmpty { - await MainActor.run { - let combinedImage = NSImage(size: totalBounds.size) - combinedImage.lockFocus() - for item in imagePartsToDraw { - item.image.draw(in: item.rect) - print("๐Ÿ–ผ Drawn screen \(item.screenName) at \(item.rect) on combined image") - } - combinedImage.unlockFocus() - - // Verberg loading indicator voordat we de preview tonen - self.previewManager.hideLoadingIndicator() - self.processCapture(image: combinedImage) - } - } else if !allCapturedSuccessfully { - await MainActor.run { - print("โŒ Error: Not all screens were captured successfully for the combined image.") - self.previewManager.hideLoadingIndicator() - } - } else { - await MainActor.run { - print("โŒ Error: No images were captured to create a combined image.") - self.previewManager.hideLoadingIndicator() - } - } - } - } - - // NIEUW: Methode om de notifier voor "alle schermen" modus bij te werken (placeholder) - func updateAllScreensModeNotifier() { - let isActive = isAllScreensCaptureToggledOn - print("๐Ÿ”” Notifier update: All Screens Mode is \(isActive ? "Active" : "Inactive")") - - for window in eventCaptureWindows { - // Update CrosshairCursorView (voor de blauwe cirkel) - if let contentView = window.contentView { - for subview in contentView.subviews { - if let crosshairView = subview as? CrosshairCursorView { - crosshairView.currentMode = isActive ? .allScreensActive : .normal - // crosshairView.needsDisplay = true // needsDisplay wordt al in didSet van currentMode afgehandeld - } - // Update EventCaptureView (voor de tekst-overlay) - if let eventCaptureView = subview as? EventCaptureView { - eventCaptureView.shouldDisplayAllScreensActiveText = isActive - eventCaptureView.needsDisplay = true - } - } - } - } - } - - // NIEUW: Reset de toggle expliciet, bijvoorbeeld bij het annuleren van een selectie - func resetAllScreensCaptureToggle() { - if isAllScreensCaptureToggledOn { - isAllScreensCaptureToggledOn = false - updateAllScreensModeNotifier() - print("๐Ÿ”„ All Screens Capture Toggled Off explicitly (e.g., selection cancelled).") - } - } - - // NIEUW: Selector method for wizard close notification - @objc private func firstLaunchWizardDidClose(_ notification: Notification) { - print("๐ŸŽ‰ First launch wizard closed") - if let wizard = notification.object as? FirstLaunchWizard { - // Remove the observer for this specific wizard - NotificationCenter.default.removeObserver(self, name: NSWindow.willCloseNotification, object: wizard) - } - firstLaunchWizard = nil - } - - // NIEUW: Reset first launch wizard (for testing) - @objc func resetFirstLaunchWizardInternal() { - print("๐Ÿ”„ Resetting first launch wizard for testing") - SettingsManager.shared.hasCompletedFirstLaunch = false - - // FIXED: Better memory management - check if wizard still exists - if let existingWizard = firstLaunchWizard { - print("๐Ÿ—‘๏ธ Closing existing wizard before showing new one") - // Remove observer first to prevent issues - NotificationCenter.default.removeObserver(self, name: NSWindow.willCloseNotification, object: existingWizard) - // Close the wizard safely - if existingWizard.isVisible { - existingWizard.close() - } - // Clear the reference immediately - firstLaunchWizard = nil - } else { - print("โ„น๏ธ No existing wizard to close") - } - - // Add small delay to ensure cleanup is complete before creating new wizard - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in - guard let self = self else { return } - print("๐ŸŽ‰ Showing wizard after reset delay") - self.checkAndShowFirstLaunchWizard() - } - } - - // NIEUW: Show Stash via menu - @objc func showStash(_ sender: Any?) { - print("๐Ÿ—‚๏ธ Show Stash menu item selected") - - // Controleer of er al een stash window open is - if let existingController = stashWindowController, - let existingWindow = existingController.window, existingWindow.isVisible { - print("๐Ÿ—‚๏ธ Stash window already open. Bringing to front.") - existingWindow.orderFrontRegardless() - existingWindow.makeKeyAndOrderFront(nil as Any?) - NSApp.activate(ignoringOtherApps: true) - return - } - - // ๐Ÿ”ฅ NIEUW: Load persistent stash images if available - print("๐Ÿ—‚๏ธ Creating new Stash window") - let newImageStore = GalleryImageStore() - - // Check for persistent stash images - if SettingsManager.shared.persistentStash, - let persistentStore = persistentStashImageStore, - persistentStore.images.count > 0 { - print("๐Ÿ”ฅ PERSISTENT STASH: Loading \(persistentStore.images.count) saved images") - newImageStore.images = persistentStore.images - } else { - print("๐Ÿ”ฅ PERSISTENT STASH: No saved images or persistent stash disabled - starting empty") - } - - let initialStashWidth: CGFloat = 200 - let initialStashHeight: CGFloat = 300 - let newWindow = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: initialStashWidth, height: initialStashHeight), - styleMask: [.borderless, .closable, .fullSizeContentView], - backing: .buffered, defer: false) - - let closeWindowAction = { [weak newWindow] in - guard let windowToClose = newWindow else { return } - windowToClose.close() - } - - let galleryView = IntegratedGalleryView(imageStore: newImageStore, initialImage: nil, hostingWindow: newWindow, closeAction: closeWindowAction) - let hostingView = NSHostingView(rootView: galleryView) - - newWindow.isOpaque = false - newWindow.backgroundColor = .clear - newWindow.hasShadow = true - newWindow.titleVisibility = .hidden - newWindow.titlebarAppearsTransparent = true - newWindow.isMovable = false - - let rootContentView = NSView(frame: newWindow.contentRect(forFrameRect: newWindow.frame)) - rootContentView.wantsLayer = true - rootContentView.layer?.cornerRadius = 12 - rootContentView.layer?.masksToBounds = true - - let visualEffectView = NSVisualEffectView() - visualEffectView.blendingMode = .behindWindow - visualEffectView.material = .hudWindow - visualEffectView.state = .active - visualEffectView.autoresizingMask = [.width, .height] - visualEffectView.frame = rootContentView.bounds - visualEffectView.wantsLayer = true - visualEffectView.layer?.cornerRadius = 12 - visualEffectView.layer?.masksToBounds = true - - let extraBlurView = NSVisualEffectView() - extraBlurView.blendingMode = .behindWindow - extraBlurView.material = .hudWindow - extraBlurView.state = .active - extraBlurView.alphaValue = 0.6 - extraBlurView.autoresizingMask = [.width, .height] - extraBlurView.frame = rootContentView.bounds - - rootContentView.addSubview(visualEffectView) - rootContentView.addSubview(extraBlurView) - hostingView.translatesAutoresizingMaskIntoConstraints = false - rootContentView.addSubview(hostingView) - - NSLayoutConstraint.activate([ - hostingView.topAnchor.constraint(equalTo: rootContentView.topAnchor), - hostingView.bottomAnchor.constraint(equalTo: rootContentView.bottomAnchor), - hostingView.leadingAnchor.constraint(equalTo: rootContentView.leadingAnchor), - hostingView.trailingAnchor.constraint(equalTo: rootContentView.trailingAnchor) - ]) - - newWindow.contentView = rootContentView - - // FIXED: Position stash window on the same screen as the main thumbnail - let targetScreen = self.getTargetScreenForStashPreview() ?? NSScreen.main ?? NSScreen.screens.first - - if let screenToDisplayOn = targetScreen { - let targetY = (screenToDisplayOn.visibleFrame.height - initialStashHeight) / 2 + screenToDisplayOn.visibleFrame.origin.y - let spacing: CGFloat = 20 - let targetX = screenToDisplayOn.visibleFrame.maxX - initialStashWidth - spacing - newWindow.setFrameOrigin(NSPoint(x: targetX, y: targetY)) - print("๐ŸชŸ Stash window positioned on screen: \(screenToDisplayOn.customLocalizedName) at {\(targetX), \(targetY)}") - } else { - newWindow.center() // Fallback - print("๐ŸชŸ Stash window centered (could not determine target screen).") - } - - newWindow.delegate = self - let newWindowController = NSWindowController(window: newWindow) - newWindowController.showWindow(self) - self.stashWindowController = newWindowController - self.activeStashImageStore = newImageStore - - // Set window level based on always on top setting - if SettingsManager.shared.stashAlwaysOnTop { - newWindow.level = .floating - print("๐Ÿ” New empty stash window created with always on top") - } else { - newWindow.level = .normal - print("๐Ÿ“‹ New empty stash window created with normal level") - } - - newWindow.makeKeyAndOrderFront(nil as Any?) - NSApp.activate(ignoringOtherApps: true) - - // ๐Ÿ”ฅ NIEUW: Shake animatie ook voor Menu "Show Stash" windows! - print("๐ŸŽ‰ Adding MENU stash shake animation!") - addSubtleShakeAnimation(to: newWindow) - - print("๐Ÿ—‚๏ธ Empty Stash window created and shown") - } - - // NIEUW: Update stash window level based on setting - func updateStashWindowLevel() { - guard let stashWindow = stashWindowController?.window else { return } - - if SettingsManager.shared.stashAlwaysOnTop { - stashWindow.level = .floating - print("๐Ÿ” Stash window set to always on top") - } else { - stashWindow.level = .normal - print("๐Ÿ“‹ Stash window set to normal level") - } - } - - func applicationWillTerminate(_ notification: Notification) { - print("๐Ÿ” DEBUG: applicationWillTerminate called") - - previewDismissTimer?.invalidate() - previewDismissTimer = nil - - // NIEUW: Cleanup hotkey reset timer - hotkeyResetTimer?.invalidate() - hotkeyResetTimer = nil - - // ๐Ÿ”ง FIX: Cleanup double-click action timer - doubleClickActionTimer?.invalidate() - doubleClickActionTimer = nil - - // ๐Ÿงช NIEUW: Cleanup cache cleanup timer - cacheCleanupTimer?.invalidate() - cacheCleanupTimer = nil - - // NIEUW: Cleanup FirstLaunchWizard properly - if let wizard = firstLaunchWizard { - NotificationCenter.default.removeObserver(self, name: NSWindow.willCloseNotification, object: wizard) - wizard.close() - firstLaunchWizard = nil - } - - NotificationCenter.default.removeObserver(self) // Verwijder alle observers - - // MARK: - Multi-Monitor Cleanup - cleanupMultiMonitorResources() - - // NIEUW: Cleanup MenuManager - menuManager?.cleanup() - - // NIEUW: Cleanup PreviewManager - previewManager?.cleanup() - - // NIEUW: Cleanup FinderWindowManager - finderWindowManager?.forceCleanup() - } - - // MARK: - Menu functions moved to MenuManager.swift - - func activateMultiMonitorSelection() { - // Sluit een eventueel openstaand rename panel voordat een nieuwe selectie start - renameActionHandler.closeRenamePanelAndCleanup() - - // NIEUW: Reset de "alle schermen" toggle bij het starten van een nieuwe selectie - resetAllScreensCaptureToggle() - - // NIEUW: Reset isDragging state voor een schone start - self.isDragging = false - - // NIEUW: Sluit ook een eventueel bestaande activePreviewWindow direct - if let existingPreview = self.activePreviewWindow, existingPreview.isVisible { - print("๐Ÿ”ต Closing existing activePreviewWindow before starting new selection.") - self.closePreviewWithAnimation(immediate: true, preserveTempFile: false) - } - - // NIEUW: Ruim een eventueel bestaande actieve overlay op - if let existingOverlay = activeOverlayWindow, existingOverlay.isVisible { - print("๐Ÿ”ต Closing existing active overlay window before starting new selection.") - existingOverlay.orderOut(nil as Any?) - activeOverlayWindow = nil - } - - // ๐ŸŽฏ NIEUWE OPLOSSING: Gebruik native macOS screencapture in plaats van complexe crosshair - triggerNativeScreencapture() - } - - // ๐ŸŽฏ VASTE OPLOSSING: Multi-hotkey tracking met correcte timing - private var hotkeyPressCount: Int = 0 - private var firstHotkeyPressTime: Date = Date.distantPast - private var hotkeyResetTimer: Timer? - private let multiHotkeyTimeWindow: TimeInterval = 1.5 // 1.5 seconde venster voor betrouwbare multi-click detectie - - // ๐Ÿ”ง FIX: Remember mouse location from first hotkey press for accurate multi-screen detection - private var rememberedMouseLocation: NSPoint = NSPoint.zero - - private func triggerNativeScreencapture() { - let now = Date() - let timeSinceFirstPress = now.timeIntervalSince(firstHotkeyPressTime) - - // Reset counter als te veel tijd verstreken is - if timeSinceFirstPress > multiHotkeyTimeWindow { - hotkeyPressCount = 0 - firstHotkeyPressTime = now - } - - // Verhoog counter - hotkeyPressCount += 1 - - // Als dit de eerste press is, stel eerste tijd in en onthoud mouse location - if hotkeyPressCount == 1 { - firstHotkeyPressTime = now - // ๐Ÿ”ง FIX: Remember mouse location from first press for accurate multi-screen detection - rememberedMouseLocation = NSEvent.mouseLocation - print("๐Ÿ–ฑ๏ธ Remembered mouse location for multi-click: \(rememberedMouseLocation)") - } - - print("๐ŸŽฏ Hotkey press count: \(hotkeyPressCount), time since first: \(timeSinceFirstPress)") - - // ๐Ÿ”ง FIX: Multi-click detection EERST - boven desktop filtering check - // Cancel any existing reset timer - hotkeyResetTimer?.invalidate() - - if hotkeyPressCount == 3 { - print("๐ŸŽฏ๐ŸŽฏ๐ŸŽฏ TRIPLE hotkey press detected - capturing ALL SCREENS!") - print("๐Ÿ”ง Triple hotkey bypasses desktop filtering settings - using NATIVE mode") - // ๐Ÿ”ง CRITICAL FIX: Deactivate multi-monitor selection mode IMMEDIATELY - deactivateMultiMonitorSelection() - // ๐Ÿ”ง FIX: KILL active screencapture immediately to remove crosshair - killActiveScreencapture() - // ๐Ÿ”ง FIX: Cancel any pending double-click action - doubleClickActionTimer?.invalidate() - doubleClickActionTimer = nil - print("๐Ÿ”ง Cancelled pending double-click action due to triple-click") - // ๐Ÿ”ง FIX: Delay reset to allow for any rapid subsequent clicks - scheduleDelayedReset() - captureAllScreensNative() - return - } else if hotkeyPressCount == 2 { - print("๐ŸŽฏ๐ŸŽฏ Double hotkey press detected - capturing whole screen under cursor!") - print("๐Ÿ”ง Double hotkey bypasses desktop filtering settings - using NATIVE mode") - print("๐Ÿ”ง Waiting for potential 3rd click before executing...") - // ๐Ÿ”ง CRITICAL FIX: Deactivate multi-monitor selection mode IMMEDIATELY - deactivateMultiMonitorSelection() - // ๐Ÿ”ง FIX: KILL active screencapture immediately to remove crosshair - killActiveScreencapture() - // ๐Ÿ”ง FIX: Schedule 2x action with delay to allow 3rd click to interrupt - scheduleDelayedDoubleClickAction() - return - } - - // ๐Ÿ”ง First press only: Check desktop filtering for single press behavior - // Start timer to reset if no second press comes - startHotkeyResetTimer() - - // ๐ŸŽฏ HYBRIDE SYSTEEM: Check of desktop filtering nodig is (alleen voor eerste press) - let needsDesktopFiltering = SettingsManager.shared.hideDesktopIconsDuringScreenshot || - SettingsManager.shared.hideDesktopWidgetsDuringScreenshot - - if needsDesktopFiltering { - // Gebruik ScreenCaptureKit interface voor filtering - print("๐ŸŽฏ Desktop filtering enabled - using ScreenCaptureKit selection mode") - print("๐Ÿ‘† Press Cmd+Shift+4 again within 1.2 seconds for whole screen capture") - print("๐Ÿ‘†๐Ÿ‘† Press Cmd+Shift+4 THREE times for all screens capture!") - print("๐ŸŽฏ Use drag selection for area capture (with desktop filtering)") - - continueActivateMultiMonitorSelection() - } else { - // Gebruik native screencapture voor normale macOS ervaring - print("๐ŸŽฏ Normal mode - using native macOS screencapture with crosshair cursor") - print("๐Ÿ‘† Press Cmd+Shift+4 again within 1.2 seconds for whole screen capture") - print("๐Ÿ‘†๐Ÿ‘† Press Cmd+Shift+4 THREE times for all screens capture!") - print("๐ŸŽฏ Use normal drag selection with native crosshair") - - startNativeScreencapture() - } - } - - // ๐ŸŽฏ NIEUWE FUNCTIE: Start native macOS screencapture - private func startNativeScreencapture() { - // Create temporary file for screenshot - let tempDirectory = FileManager.default.temporaryDirectory - let tempFileName = "ShotScreen_temp_\(UUID().uuidString).png" - let tempFileURL = tempDirectory.appendingPathComponent(tempFileName) - - print("๐Ÿ“„ Using temporary file: \(tempFileURL.path)") - - // Start screencapture to file (NO clipboard) - let task = Process() - task.launchPath = "/usr/sbin/screencapture" - task.arguments = ["-i", "-x", tempFileURL.path] // interactive, no sounds, save to file - - do { - try task.run() - - // Wait for screencapture completion in background - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - task.waitUntilExit() - - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - // Check if file was created (indicates successful capture) - if FileManager.default.fileExists(atPath: tempFileURL.path) { - print("โœ… Native screenshot saved to file - processing...") - self.processFileImage(at: tempFileURL) - } else { - print("๐Ÿšซ No screenshot file - user cancelled or no selection made") - } - } - } - } catch { - print("โŒ Failed to start screencapture: \(error)") - } - } - - // ๐ŸŽฏ NIEUWE FUNCTIE: Process file image (no clipboard pollution) - private func processFileImage(at fileURL: URL) { - if let image = NSImage(contentsOf: fileURL) { - print("โœ… Successfully loaded image from file") - processCapture(image: image) - - // Clean up temporary file - try? FileManager.default.removeItem(at: fileURL) - print("๐Ÿ—‘๏ธ Temporary screenshot file cleaned up") - } else { - print("โš ๏ธ Failed to load image from file") - // Clean up failed file attempt - try? FileManager.default.removeItem(at: fileURL) - } - } - - // ๐ŸŽฏ LEGACY FUNCTIE: Process clipboard image (for fallback compatibility) - private func processClipboardImage() { - let pasteboard = NSPasteboard.general - - if let image = NSImage(pasteboard: pasteboard) { - print("โœ… Successfully captured image from clipboard") - processCapture(image: image) - } else { - print("โš ๏ธ No image found in clipboard - user may have cancelled") - } - } - - // NIEUWE FUNCTIE: Capture whole screen onder cursor - private func captureWholeScreenUnderCursor() { - let mouseLocation = NSEvent.mouseLocation - print("๐Ÿ–ฑ๏ธ Mouse location: \(mouseLocation)") - - // ๐Ÿ”ง CRITICAL FIX: Kill active screencapture immediately to remove crosshair - killActiveScreencapture() - - if let targetScreen = findScreenContaining(point: mouseLocation) { - print("๐Ÿ–ฅ๏ธ Found target screen: \(targetScreen.customLocalizedName)") - print("๐Ÿ“ธ Capturing whole screen...") - - // ๐ŸŽฏ Check of desktop filtering nodig is - let needsDesktopFiltering = SettingsManager.shared.hideDesktopIconsDuringScreenshot || - SettingsManager.shared.hideDesktopWidgetsDuringScreenshot - - if needsDesktopFiltering { - // Gebruik ScreenCaptureKit voor filtering - print("๐ŸŽฏ Using ScreenCaptureKit for whole screen capture (desktop filtering enabled)") - Task { [weak self] in - guard let self = self else { return } - - // Intel Mac compatible ScreenCaptureKit usage - if let provider = self.screenCaptureProvider { - let windowsToExclude = await provider.getAllWindowsToExclude() - - if let image = await provider.captureScreen(screen: targetScreen, excludingWindows: windowsToExclude) { - await MainActor.run { - print("โœ… Whole screen captured successfully with filtering!") - self.processCapture(image: image) - } - } else { - print("โŒ Failed to capture whole screen with filtering") - } - } else { - // Fallback to native screencapture for Intel Macs without ScreenCaptureKit - print("โšก Intel Mac fallback: Using native screencapture for whole screen") - await MainActor.run { - self.captureWholeScreenNative(screen: targetScreen) - } - } - } - } else { - // Gebruik native screencapture voor hele scherm - print("๐ŸŽฏ Using native screencapture for whole screen capture") - captureWholeScreenNative(screen: targetScreen) - } - } else { - print("โŒ No target screen found for location: \(mouseLocation)") - } - } - - // ๐Ÿ”ง NIEUWE MULTI-CLICK FUNCTIE: Direct native whole screen capture (bypasses desktop filtering) - private func captureWholeScreenNativeMultiClick() { - print("๐ŸŽฏ DEBUG: captureWholeScreenNativeMultiClick CALLED!") - - // ๐Ÿ”ง CRITICAL FIX: Deactivate multi-monitor selection mode IMMEDIATELY - deactivateMultiMonitorSelection() - - // ๐Ÿ”ง FIX: Use remembered mouse location from first hotkey press for accurate multi-screen detection - let mouseLocation = rememberedMouseLocation - print("๐Ÿ–ฑ๏ธ Multi-click using REMEMBERED mouse location: \(mouseLocation)") - print("๐Ÿ”ง Current mouse location would be: \(NSEvent.mouseLocation)") - - // ๐Ÿ”ง CRITICAL FIX: Kill active screencapture immediately to remove crosshair - killActiveScreencapture() - - if let targetScreen = findScreenContaining(point: mouseLocation) { - print("๐Ÿ–ฅ๏ธ Multi-click found target screen: \(targetScreen.customLocalizedName)") - print("๐Ÿ“ธ Multi-click capturing whole screen DIRECTLY with native screencapture...") - - // ๐Ÿ”ง DIRECT NATIVE - NO DESKTOP FILTERING CHECK - captureWholeScreenNative(screen: targetScreen) - } else { - print("โŒ Multi-click: No target screen found for REMEMBERED location: \(mouseLocation)") - print("โš ๏ธ Falling back to main screen for multi-click capture") - // Fallback to main screen if remembered location doesn't work - if let mainScreen = NSScreen.main { - captureWholeScreenNative(screen: mainScreen) - } - } - } - - // ๐ŸŽฏ NIEUWE FUNCTIE: Native whole screen capture - private func captureWholeScreenNative(screen: NSScreen) { - let displayID = screen.displayID - - // ๐Ÿ”ง FIX: Get screen index for screencapture (1-based) instead of display ID - guard let screenIndex = NSScreen.screens.firstIndex(of: screen) else { - print("โŒ Could not find screen index for \(screen.customLocalizedName)") - return - } - let screencaptureDisplayNumber = screenIndex + 1 // screencapture is 1-based - - print("๐ŸŽฏ DEBUG: captureWholeScreenNative called") - print(" Screen: \(screen.customLocalizedName)") - print(" Frame: \(screen.frame)") - print(" Display ID: \(displayID)") - print(" Screen Index: \(screenIndex)") - print(" Screencapture Display Number: \(screencaptureDisplayNumber)") - - // Create temporary file for screenshot - let tempDirectory = FileManager.default.temporaryDirectory - let tempFileName = "ShotScreen_wholescreen_\(UUID().uuidString).png" - let tempFileURL = tempDirectory.appendingPathComponent(tempFileName) - - print("๐Ÿ“„ Using temporary file: \(tempFileURL.path)") - - // Start screencapture for specific display (NO clipboard) - let task = Process() - task.launchPath = "/usr/sbin/screencapture" - task.arguments = ["-D", "\(screencaptureDisplayNumber)", "-x", tempFileURL.path] // ๐Ÿ”ง FIX: No clipboard, save to file - - print("๐Ÿ”ง DEBUG: Starting screencapture with arguments: \(task.arguments ?? [])") - - do { - try task.run() - - // Wait for completion - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - task.waitUntilExit() - - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - // Check if file was created (indicates successful capture) - if FileManager.default.fileExists(atPath: tempFileURL.path) { - print("โœ… Native whole screen captured successfully!") - self.processFileImage(at: tempFileURL) - } else { - print("โŒ No whole screen screenshot file created") - } - } - } - } catch { - print("โŒ Failed to start whole screen screencapture: \(error)") - } - } - - - // NIEUWE FUNCTIE: Vind scherm waar punt zich bevindt - private func findScreenContaining(point: NSPoint) -> NSScreen? { - print("๐Ÿ” DEBUG: Looking for screen containing point: \(point)") - print("๐Ÿ” DEBUG: Available screens:") - for (index, screen) in NSScreen.screens.enumerated() { - print(" Screen \(index): \(screen.customLocalizedName) - Frame: \(screen.frame)") - if screen.frame.contains(point) { - print(" โœ… MATCH! Screen \(index) contains the point") - return screen - } else { - print(" โŒ Screen \(index) does NOT contain the point") - } - } - print("โš ๏ธ DEBUG: No screen found containing point, falling back to main screen") - return NSScreen.main // fallback naar main screen - } - - // ๐Ÿ”ง CRITICAL FUNCTION: Kill active screencapture to remove crosshair cursor - private func killActiveScreencapture() { - print("โšก Killing active screencapture to remove crosshair cursor...") - let killTask = Process() - killTask.launchPath = "/usr/bin/killall" - killTask.arguments = ["screencapture"] - - do { - try killTask.run() - killTask.waitUntilExit() - print("โšก Active screencapture killed, crosshair cursor should be restored to normal") - } catch { - print("โš ๏ธ Failed to kill screencapture: \(error)") - } - } - - // NIEUWE FUNCTIES: Hotkey counter management - private func resetHotkeyCounter() { - hotkeyPressCount = 0 - firstHotkeyPressTime = Date.distantPast - hotkeyResetTimer?.invalidate() - hotkeyResetTimer = nil - // ๐Ÿ”ง FIX: Also cleanup double-click action timer - doubleClickActionTimer?.invalidate() - doubleClickActionTimer = nil - // ๐Ÿ”ง FIX: Reset remembered mouse location - rememberedMouseLocation = NSPoint.zero - print("๐Ÿ”„ Hotkey counter reset") - } - - private func startHotkeyResetTimer() { - hotkeyResetTimer?.invalidate() - hotkeyResetTimer = Timer.scheduledTimer(withTimeInterval: multiHotkeyTimeWindow, repeats: false) { [weak self] _ in - self?.resetHotkeyCounter() - } - } - - // ๐Ÿ”ง NEW: Schedule delayed reset to allow rapid multi-clicks - private func scheduleDelayedReset() { - hotkeyResetTimer?.invalidate() - hotkeyResetTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { [weak self] _ in - self?.resetHotkeyCounter() - print("๐Ÿ”„ Delayed hotkey counter reset after multi-click action") - } - } - - // ๐Ÿ”ง NEW: Timer for delayed double-click action - private var doubleClickActionTimer: Timer? - - // ๐Ÿ”ง NEW: Schedule delayed double-click action to allow 3rd click to interrupt - private func scheduleDelayedDoubleClickAction() { - print("โฒ๏ธ DEBUG: scheduleDelayedDoubleClickAction CALLED!") - doubleClickActionTimer?.invalidate() - doubleClickActionTimer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { [weak self] _ in - guard let self = self else { return } - print("๐ŸŽฏ๐ŸŽฏ DEBUG: Executing delayed double-click action (no 3rd click detected)") - self.captureWholeScreenNativeMultiClick() - self.scheduleDelayedReset() - } - } - - - - - - - - - private func continueActivateMultiMonitorSelection() { - // VERWIJDERD: Herstel Finder vensters - niet meer nodig zonder Finder restart - // if SettingsManager.shared.cleanDesktopScreenshots { - // finderWindowManager.restoreFinderWindows() - // } - - // Hide any existing multi-monitor overlay windows - hideMultiMonitorOverlayWindows() - - // Create event capture window to intercept all mouse events - createEventCaptureWindow() - - // Activate multi-monitor selection mode - isMultiMonitorSelectionActive = true - - // Hide system cursor and setup custom crosshair overlay - hideCursor() - setupCustomCrosshairOverlay() - - // Add global key monitor for ESC key - setupGlobalKeyMonitor() - - print("๐Ÿš€ Multi-monitor selection mode activated!") - print("๐ŸŽฏ Click and drag across multiple monitors for seamless selection") - print("๐Ÿ”ง Press ESC or right-click to cancel selection") - } - - func beginSelection() { - // Legacy method - redirect to new multi-monitor system - activateMultiMonitorSelection() - } - - func capture(rect: NSRect, isMultiMonitor: Bool = false) { - print("๐Ÿ“ธ capture gestart") - print("๐Ÿ“ Capture gebied: x=\(rect.origin.x), y=\(rect.origin.y), width=\(rect.width), height=\(rect.height)") - - // ๐Ÿ”ง CRITICAL FIX: Kill any active crosshair cursor immediately when capture starts - killActiveScreencapture() - - // ๐Ÿ”ง CRITICAL FIX: Deactivate multi-monitor selection mode to remove custom crosshair overlay - deactivateMultiMonitorSelection() - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in - guard let self = self else { return } - - if isMultiMonitor { - // Use specialized multi-monitor capture - self.captureMultiMonitorRegion(rect: rect) - } else { - // Use traditional single-screen capture - self.captureSingleScreen(rect: rect) - } - } - } - - private func captureSingleScreen(rect: NSRect) { - guard let screen = NSScreen.main else { return } - - NSLog("๐ŸŽฏ Single-screen capture (main screen selection): \(rect)") - - Task { [weak self] in - guard let self = self else { return } - - // Intel Mac compatible ScreenCaptureKit usage - if let provider = self.screenCaptureProvider { - let windowsToExclude = await provider.getAllWindowsToExclude() - let selectionCGRect = CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.width, height: rect.height) - - if let image = await provider.captureSelection(selectionRectInPoints: selectionCGRect, screen: screen, excludingWindows: windowsToExclude) { - await MainActor.run { - self.processCapture(image: image) - } - } else { - NSLog("Error: Failed to capture main screen selection using ScreenCaptureKitProvider") - } - } else { - // Fallback to native screencapture for Intel Macs without ScreenCaptureKit - print("โšก Intel Mac fallback: Single screen selection capture not supported - using full screen") - await MainActor.run { - self.captureCurrentScreenNative(screen: screen) - } - } - } - } - - // Helper function to flip image vertically (from display coordinates to AppKit coordinates) - private func flipImageVertically(_ image: NSImage) -> NSImage { - let flippedImage = NSImage(size: image.size) - flippedImage.lockFocus() - - // Create a transform that flips vertically - let transform = NSAffineTransform() - transform.translateX(by: 0, yBy: image.size.height) - transform.scaleX(by: 1.0, yBy: -1.0) - transform.concat() - - // Draw the original image with the transform applied - image.draw(at: .zero, from: .zero, operation: .sourceOver, fraction: 1.0) - - flippedImage.unlockFocus() - return flippedImage - } - - private func captureMultiMonitorRegion(rect: NSRect) { - print("๐Ÿ”„ Multi-monitor capture: analyzing screen intersections") - - // Find all screens that intersect with the capture region - let intersectingScreens = NSScreen.screens.compactMap { screen -> (screen: NSScreen, intersection: NSRect)? in - let intersection = rect.intersection(screen.frame) - if intersection.width > 1 && intersection.height > 1 { - return (screen: screen, intersection: intersection) - } - return nil - }.sorted { first, second in - // Sort by Y coordinate (higher Y = higher on screen = should be at top of final image) - return first.intersection.midY > second.intersection.midY - } - - print("๐Ÿ“บ Found \(intersectingScreens.count) intersecting screens:") - for (index, screenInfo) in intersectingScreens.enumerated() { - print(" \(index): \(screenInfo.screen.customLocalizedName) - intersection: \(screenInfo.intersection)") // GEWIJZIGD - } - - if intersectingScreens.isEmpty { - print("โŒ No screens intersect with capture region") - return - } - - if intersectingScreens.count == 1 { - // Single screen, capture directly - let screenInfo = intersectingScreens[0] - self.captureSingleScreenRegion(screen: screenInfo.screen, region: screenInfo.intersection) - } else { - // Multiple screens, capture each and combine - self.captureAndCombineScreens(intersectingScreens: intersectingScreens, totalRect: rect) - } - } - - private func captureSingleScreenRegion(screen: NSScreen, region: NSRect) { - NSLog("๐Ÿ“ธ Capturing single screen region on \(screen.customLocalizedName), region: \(region)") - - Task { [weak self] in - guard let self = self else { return } - - // Intel Mac compatible ScreenCaptureKit usage - if let provider = self.screenCaptureProvider { - let windowsToExclude = await provider.getAllWindowsToExclude() - let selectionCGRect = CGRect(x: region.origin.x, y: region.origin.y, width: region.width, height: region.height) - - if let image = await provider.captureSelection(selectionRectInPoints: selectionCGRect, screen: screen, excludingWindows: windowsToExclude) { - await MainActor.run { - self.processCapture(image: image) - } - } else { - NSLog("Error: Failed to capture screen region on \(screen.customLocalizedName) using ScreenCaptureKitProvider") - } - } else { - // Fallback to native screencapture for Intel Macs without ScreenCaptureKit - print("โšก Intel Mac fallback: Single screen region capture not supported - using full screen") - await MainActor.run { - self.captureCurrentScreenNative(screen: screen) - } - } - } - } - - private func captureAndCombineScreens(intersectingScreens: [(screen: NSScreen, intersection: NSRect)], totalRect: NSRect) { - NSLog("๐Ÿ”„ Capturing and combining \(intersectingScreens.count) screens for totalRect: \(totalRect)") - - let canvasSize = NSSize(width: totalRect.width, height: totalRect.height) - - // Show loading indicator for multi-screen stitching - previewManager.showLoadingIndicator() - - Task { [weak self] in - guard let self = self else { return } - - let combinedImage = await self.createCombinedImage(canvasSize: canvasSize) - await self.captureAndStitchScreenParts(intersectingScreens: intersectingScreens, - totalRect: totalRect, - combinedImage: combinedImage) - } - } - - // MARK: - Private Helper Methods for Multi-Screen Capture - - private func createCombinedImage(canvasSize: NSSize) async -> NSImage { - return await MainActor.run { NSImage(size: canvasSize) } - } - - private func captureAndStitchScreenParts(intersectingScreens: [(screen: NSScreen, intersection: NSRect)], - totalRect: NSRect, - combinedImage: NSImage) async { - var allPartsCaptured = true - - for screenInfo in intersectingScreens { - let captureSuccessful = await captureScreenPart(screenInfo: screenInfo, - totalRect: totalRect, - combinedImage: combinedImage) - if !captureSuccessful { - allPartsCaptured = false - break - } - } - - await handleCombinedCaptureResult(allPartsCaptured: allPartsCaptured, - intersectingScreens: intersectingScreens, - combinedImage: combinedImage) - } - - private func captureScreenPart(screenInfo: (screen: NSScreen, intersection: NSRect), - totalRect: NSRect, - combinedImage: NSImage) async -> Bool { - let screen = screenInfo.screen - let intersection = screenInfo.intersection - - NSLog("๐Ÿ“ธ Capturing part from screen: \(screen.customLocalizedName), intersection: \(intersection)") - - let selectionCGRect = CGRect(x: intersection.origin.x, y: intersection.origin.y, - width: intersection.width, height: intersection.height) - - // Intel Mac compatible ScreenCaptureKit usage - guard let provider = self.screenCaptureProvider else { - print("โšก Intel Mac fallback: Multi-screen capture not supported without ScreenCaptureKit") - return false - } - - let windowsToExclude = await provider.getAllWindowsToExclude() - - guard let partImage = await provider.captureSelection( - selectionRectInPoints: selectionCGRect, - screen: screen, - excludingWindows: windowsToExclude - ) else { - NSLog("Error: Failed to capture part of screen \(screen.customLocalizedName) using ScreenCaptureKitProvider for intersection \(intersection)") - return false - } - - await stitchImagePart(partImage: partImage, intersection: intersection, - totalRect: totalRect, combinedImage: combinedImage, - screenName: screen.customLocalizedName) - return true - } - - private func stitchImagePart(partImage: NSImage, intersection: NSRect, - totalRect: NSRect, combinedImage: NSImage, - screenName: String) async { - await MainActor.run { - combinedImage.lockFocus() - let drawRect = NSRect( - x: intersection.origin.x - totalRect.origin.x, - y: intersection.origin.y - totalRect.origin.y, - width: intersection.width, - height: intersection.height - ) - partImage.draw(in: drawRect) - combinedImage.unlockFocus() - NSLog("๐Ÿ–ผ Drawn part from \(screenName) at \(drawRect) on combined image") - } - } - - private func handleCombinedCaptureResult(allPartsCaptured: Bool, - intersectingScreens: [(screen: NSScreen, intersection: NSRect)], - combinedImage: NSImage) async { - await MainActor.run { - self.previewManager.hideLoadingIndicator() - - if allPartsCaptured && !intersectingScreens.isEmpty { - self.processCapture(image: combinedImage) - } else if intersectingScreens.isEmpty { - NSLog("Error: No intersecting screens found for combined capture.") - } else { - NSLog("Error: Not all parts captured successfully for combined image.") - } - } - } - - func processCapture(image: NSImage) { - // VERWIJDERD: Desktop icon restore functionality - feature disabled - - if SettingsManager.shared.playSoundOnCapture { - let soundType = SettingsManager.shared.screenshotSoundType - let volume = SettingsManager.shared.screenshotSoundVolume - - if let sound = NSSound(named: soundType.systemSoundName) { - sound.volume = volume - sound.play() - } else { - print("โš ๏ธ Could not load sound '\(soundType.systemSoundName)' - falling back to Pop") - // Fallback to Pop if selected sound doesn't exist - if let fallbackSound = NSSound(named: "Pop") { - fallbackSound.volume = volume - fallbackSound.play() - } - } - } - - if let pngData = self.createPngData(from: image) { - let tempURL = self.createTempUrl() - do { - try pngData.write(to: tempURL) - print("๐Ÿ“ธ Capture: Setting tempURL to \(tempURL.path)") - self.setTempFileURL(tempURL) - print("๐Ÿ’พ Tijdelijk bestand aangemaakt: \(tempURL.path)") - print("๐Ÿ“Š PNG data grootte: \(pngData.count) bytes") - - // ๐ŸŽฏ NEW: Clear old thumbnail restoration backup before creating new one - self.clearBackupAfterNewScreenshot() - - // ๐ŸŽฏ NEW: Create thumbnail restoration backup - self.createImageBackup(image: image, tempURL: tempURL) - - if SettingsManager.shared.autoSaveScreenshot { - print("โš™๏ธ Automatisch opslaan is AAN.") - self.saveDirectlyToConfiguredFolder() - if !SettingsManager.shared.closeAfterSave { - self.lastImage = image - self.showPreview(image: image) - } - } else { - print("โš™๏ธ Automatisch opslaan is UIT. Preview wordt getoond.") - self.lastImage = image - self.showPreview(image: image) - } - } catch { - print("โŒ Fout bij opslaan tijdelijk bestand: \(error)") - } - } - } - - private func createPngData(from image: NSImage) -> Data? { - guard let tiffData = image.tiffRepresentation, - let bitmap = NSBitmapImageRep(data: tiffData) else { return nil } - return bitmap.representation(using: .png, properties: [:]) - } - - internal func createTempUrl() -> URL { - let filenameBase = generateFilename() - // GEWIJZIGD: Gebruik permanente thumbnail directory in plaats van temp directory - return thumbnailDirectory - .appendingPathComponent(filenameBase) - .appendingPathExtension("png") - } - - internal func generateFilename() -> String { - let prefix = SettingsManager.shared.filenamePrefix - let preset = SettingsManager.shared.filenameFormatPreset - let customFormat = SettingsManager.shared.filenameCustomFormat - let now = Date() - let dateFormatter = DateFormatter() - var filename = prefix - - switch preset { - case .macOSStyle: - dateFormatter.dateFormat = "yyyy-MM-dd HH.mm.ss" - let dateString = dateFormatter.string(from: now) - if prefix == "Schermafbeelding" { - filename = "Schermafbeelding \(dateString)" - } else { - filename += (prefix.isEmpty ? "" : " ") + dateString - } - case .compactDateTime: - dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss" - filename += (prefix.isEmpty ? "" : "_") + dateFormatter.string(from: now) - case .superCompactDateTime: - dateFormatter.dateFormat = "yyyyMMdd_HHmmss" - filename += (prefix.isEmpty ? "" : "_") + dateFormatter.string(from: now) - case .timestamp: - filename += (prefix.isEmpty ? "" : "_") + String(Int(now.timeIntervalSince1970)) - case .prefixOnly: - if filename.isEmpty { filename = "screenshot" } - case .custom: - var format = customFormat - dateFormatter.dateFormat = "yyyy" - format = format.replacingOccurrences(of: "{YYYY}", with: dateFormatter.string(from: now)) - dateFormatter.dateFormat = "yy" - format = format.replacingOccurrences(of: "{YY}", with: dateFormatter.string(from: now)) - dateFormatter.dateFormat = "MM" - format = format.replacingOccurrences(of: "{MM}", with: dateFormatter.string(from: now)) - dateFormatter.dateFormat = "dd" - format = format.replacingOccurrences(of: "{DD}", with: dateFormatter.string(from: now)) - dateFormatter.dateFormat = "HH" - format = format.replacingOccurrences(of: "{hh}", with: dateFormatter.string(from: now)) - dateFormatter.dateFormat = "mm" - format = format.replacingOccurrences(of: "{mm}", with: dateFormatter.string(from: now)) - dateFormatter.dateFormat = "ss" - format = format.replacingOccurrences(of: "{ss}", with: dateFormatter.string(from: now)) - dateFormatter.dateFormat = "SSS" - format = format.replacingOccurrences(of: "{ms}", with: dateFormatter.string(from: now)) - - if !prefix.isEmpty && !format.isEmpty && !["_", "-", " "].contains(where: { format.hasPrefix($0) }) { - filename += "_" - } - filename += format - if filename.isEmpty { filename = "custom_screenshot" } - } - - // FIXED: Ensure we always have a valid filename with fallback - if filename.isEmpty { - filename = "Schermafbeelding \(dateFormatter.string(from: now))" - } - - return filename.replacingOccurrences(of: ":", with: ".") - .replacingOccurrences(of: "/", with: "-") - } - - func showPreview(image: NSImage) { - // NIEUW: Delegate to PreviewManager - previewManager.showPreview(image: image) - // Update activePreviewWindow reference for compatibility - activePreviewWindow = previewManager.getActivePreviewWindow() - } - - func updatePreviewSize() { - // NIEUW: Delegate to PreviewManager - previewManager.updatePreviewSize() - // Update activePreviewWindow reference for compatibility - activePreviewWindow = previewManager.getActivePreviewWindow() - } - - func closePreviewWithAnimation(immediate: Bool, preserveTempFile: Bool) { - // NIEUW: Delegate to PreviewManager - previewManager.closePreviewWithAnimation(immediate: immediate, preserveTempFile: preserveTempFile) - // Update activePreviewWindow reference for compatibility - activePreviewWindow = previewManager.getActivePreviewWindow() - } - - @objc func closePreviewWindow() { - // NIEUW: Delegate to PreviewManager - previewManager.closePreviewWindow() - // Update activePreviewWindow reference for compatibility - activePreviewWindow = previewManager.getActivePreviewWindow() - } - - @objc func openSettings(_ sender: Any?) { // Any? om ook vanuit menu te kunnen aanroepen - _openSettings(sender) - } - - // Implementatie voor GridViewManagerDelegate - func getActiveWindowForGridPositioning() -> NSWindow? { - return self.activePreviewWindow // Gebruik het standaard preview venster voor positionering van de grid - } - - // Functie om een nieuw gallery venster te openen, of een bestaande te updaten - // MOVED TO GridActionManagerDelegate extension - showOrUpdateStash - - // MARK: - Private stash implementation helpers - - // AANGEPAST: Accepteert nu onScreen parameter - private func _showOrUpdateStash(with imageURL: URL?, image: NSImage? = nil, imageStore providedStore: GalleryImageStore? = nil, onScreen: NSScreen?) { - let callId = UUID().uuidString.prefix(4) - print("๐Ÿ—‚๏ธ [\(callId)] _showOrUpdateStash CALLED. imageURL: \(imageURL?.lastPathComponent ?? "nil"), image isNil: \(image == nil), providedStore isNil: \(providedStore == nil), onScreen: \(onScreen?.localizedName ?? "nil")") - - let currentImage = image ?? (imageURL != nil ? NSImage(contentsOf: imageURL!) : nil) - print("๐Ÿ—‚๏ธ [\(callId)] currentImage isNil: \(currentImage == nil)") - - if let existingController = stashWindowController, - let existingWindow = existingController.window, existingWindow.isVisible, - let existingStore = activeStashImageStore { - print("๐Ÿ—‚๏ธ [\(callId)] Stash window BESTAAT. Store heeft \(existingStore.images.count) items.") - if let img = currentImage, let url = imageURL { // url is hier de imageURL parameter - print("๐Ÿ—‚๏ธ [\(callId)] Afbeelding & URL aanwezig. Gaat naar moveToStashDirectory met url: \(url.lastPathComponent)") - let stashURL = moveToStashDirectory(from: url) // url wordt gekopieerd naar stash - let suggestedNameForAdd = url.deletingPathExtension().lastPathComponent - print("๐Ÿ—‚๏ธ [\(callId)] Probeert existingStore.addImage. stashURL: \(stashURL?.lastPathComponent ?? "nil"), suggestedNameForAdd: \(suggestedNameForAdd)") - // ๐Ÿ”ฅ CRITICAL FIX: Skip duplicate check to allow BGR thumbnails to be added multiple times - existingStore.addImage(img, fileURL: stashURL, suggestedName: suggestedNameForAdd, skipDuplicateCheck: true) - - // ๐Ÿ”„ REMOVED: Hardcoded preview closing - let DraggableImageView handle this based on closeAfterDrag setting - print("๐Ÿ”„ Stash action: Preview closing handled by DraggableImageView with closeAfterDrag setting") - } else { - print("๐Ÿ—‚๏ธ [\(callId)] Geen image of imageURL meegegeven aan bestaande stash, geen addImage.") - } - existingWindow.makeKeyAndOrderFront(nil as Any?) - NSApp.activate(ignoringOtherApps: true) - - if let targetScreen = onScreen, existingWindow.screen != targetScreen { - // ... (positionering code) - print("๐Ÿ—‚๏ธ [\(callId)] Bestaande Stash window verplaatst naar scherm: \(targetScreen.customLocalizedName)") - } - return - } - - print("๐Ÿ—‚๏ธ [\(callId)] Stash window bestaat NIET. Nieuwe stash wordt aangemaakt.") - // ... (rest van de functie voor nieuwe stash, daar zit de init van IntegratedGalleryView) - - // Clean up orphaned stash files before creating new stash - do { - let appSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! - let shotScreenDirectory = appSupportDirectory.appendingPathComponent("ShotScreen") - let stashDirectory = shotScreenDirectory.appendingPathComponent("StashItems") - - if FileManager.default.fileExists(atPath: stashDirectory.path) { - let stashContents = try FileManager.default.contentsOfDirectory(at: stashDirectory, includingPropertiesForKeys: nil) - for fileURL in stashContents { - if fileURL.pathExtension == "png" { - try? FileManager.default.removeItem(at: fileURL) - print("๐Ÿงน Cleaned up orphaned stash file: \(fileURL.lastPathComponent)") - } - } - } - } catch { - print("โš ๏ธ Could not clean up stash directory: \(error)") - } - - let store = providedStore ?? GalleryImageStore() - - if let img = currentImage, let url = imageURL { - // NIEUW: Kopieer bestand van Thumbnail naar StashItems directory - let stashURL = moveToStashDirectory(from: url) - let suggestedName = url.deletingPathExtension().lastPathComponent - - // VERWIJDERD: Geen addImage call hier meer - IntegratedGalleryView doet dit - print("๐Ÿ”„ DEBUG: Copied thumbnail to stash directory: \(stashURL?.lastPathComponent ?? "nil")") - - // ๐Ÿ”„ REMOVED: Hardcoded preview closing - let DraggableImageView handle this based on closeAfterDrag setting - print("๐Ÿ”„ Stash action: Preview closing handled by DraggableImageView with closeAfterDrag setting") - - // FIXED: Pass image data to IntegratedGalleryView for single addition - showOrUpdateStashImplementation(image: img, imageURL: stashURL, suggestedName: suggestedName, imageStore: store, onScreen: onScreen) - } else { - // No image to add - showOrUpdateStashImplementation(image: nil, imageURL: nil, suggestedName: nil, imageStore: store, onScreen: onScreen) - } - } - - // NIEUW: Hulpmethode om bestanden van Thumbnail naar StashItems directory te kopiรซren - private func moveToStashDirectory(from thumbnailURL: URL) -> URL? { - // Verkrijg de stash directory (hetzelfde als in IntegratedGalleryView) - let appSupportDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! - let shotScreenDirectory = appSupportDirectory.appendingPathComponent("ShotScreen") - let stashDirectory = shotScreenDirectory.appendingPathComponent("StashItems") - - // Maak de directory aan als die niet bestaat - try? FileManager.default.createDirectory(at: stashDirectory, withIntermediateDirectories: true, attributes: nil) - - // FIXED: Behoud de originele naam en voeg alleen suffix toe bij conflict - let originalName = thumbnailURL.deletingPathExtension().lastPathComponent - let fileExtension = thumbnailURL.pathExtension - let baseStashURL = stashDirectory.appendingPathComponent("\(originalName).\(fileExtension)") - - // Controleer of er al een bestand bestaat met deze naam - var finalStashURL = baseStashURL - var counter = 1 - while FileManager.default.fileExists(atPath: finalStashURL.path) { - finalStashURL = stashDirectory.appendingPathComponent("\(originalName)_\(counter).\(fileExtension)") - counter += 1 - } - - do { - try FileManager.default.copyItem(at: thumbnailURL, to: finalStashURL) - print("โœ… Copied thumbnail from \(thumbnailURL.lastPathComponent) to stash: \(finalStashURL.lastPathComponent)") - print("๐Ÿ”„ DEBUG: Copied thumbnail to stash directory: \(finalStashURL.lastPathComponent)") - return finalStashURL - } catch { - print("โŒ Failed to copy thumbnail to stash: \(error)") - return nil - } - } - - // AANGEPAST: Accepteert nu onScreen parameter - private func showOrUpdateStashImplementation(image: NSImage?, imageURL: URL?, suggestedName: String?, imageStore: GalleryImageStore, onScreen: NSScreen?) { - // ๐Ÿ”ฅ NIEUW: Load persistent stash images if available and store is empty - if SettingsManager.shared.persistentStash, - let persistentStore = persistentStashImageStore, - persistentStore.images.count > 0, - imageStore.images.isEmpty { - print("๐Ÿ”ฅ PERSISTENT STASH: Loading \(persistentStore.images.count) saved images into new stash") - imageStore.images = persistentStore.images - } - let initialStashWidth: CGFloat = 200 - let initialStashHeight: CGFloat = 300 - let newWindow = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: initialStashWidth, height: initialStashHeight), - styleMask: [.borderless, .closable, .fullSizeContentView], - backing: .buffered, defer: false) - - let closeWindowAction = { [weak newWindow] in - guard let windowToClose = newWindow else { return } - windowToClose.close() - } - - // FIXED: Pass all data to IntegratedGalleryView for single addition - let galleryView = IntegratedGalleryView( - imageStore: imageStore, - initialImage: image, - initialImageURL: imageURL, - initialImageName: suggestedName, - hostingWindow: newWindow, - closeAction: closeWindowAction - ) - let hostingView = NSHostingView(rootView: galleryView) - - newWindow.isOpaque = false - newWindow.backgroundColor = .clear - newWindow.hasShadow = true - newWindow.titleVisibility = .hidden - newWindow.titlebarAppearsTransparent = true - newWindow.isMovable = false - - let rootContentView = NSView(frame: newWindow.contentRect(forFrameRect: newWindow.frame)) - rootContentView.wantsLayer = true - rootContentView.layer?.cornerRadius = 12 - rootContentView.layer?.masksToBounds = true - - let visualEffectView = NSVisualEffectView() - visualEffectView.blendingMode = .behindWindow - visualEffectView.material = .hudWindow - visualEffectView.state = .active - visualEffectView.autoresizingMask = [.width, .height] - visualEffectView.frame = rootContentView.bounds - visualEffectView.wantsLayer = true - visualEffectView.layer?.cornerRadius = 12 - visualEffectView.layer?.masksToBounds = true - - let extraBlurView = NSVisualEffectView() - extraBlurView.blendingMode = .behindWindow - extraBlurView.material = .hudWindow - extraBlurView.state = .active - extraBlurView.alphaValue = 0.6 - extraBlurView.autoresizingMask = [.width, .height] - extraBlurView.frame = rootContentView.bounds - - rootContentView.addSubview(visualEffectView) - rootContentView.addSubview(extraBlurView) - hostingView.translatesAutoresizingMaskIntoConstraints = false - rootContentView.addSubview(hostingView) - - NSLayoutConstraint.activate([ - hostingView.topAnchor.constraint(equalTo: rootContentView.topAnchor), - hostingView.bottomAnchor.constraint(equalTo: rootContentView.bottomAnchor), - hostingView.leadingAnchor.constraint(equalTo: rootContentView.leadingAnchor), - hostingView.trailingAnchor.constraint(equalTo: rootContentView.trailingAnchor) - ]) - - newWindow.contentView = rootContentView - - // FIXED: Position stash window on the determined screen - let screenForPositioning = onScreen ?? NSScreen.screenWithMouse() ?? NSScreen.main ?? NSScreen.screens.first - - if let screenToDisplayOn = screenForPositioning { - let targetY = (screenToDisplayOn.visibleFrame.height - initialStashHeight) / 2 + screenToDisplayOn.visibleFrame.origin.y - let spacing: CGFloat = 20 - let targetX = screenToDisplayOn.visibleFrame.maxX - initialStashWidth - spacing - newWindow.setFrameOrigin(NSPoint(x: targetX, y: targetY)) - print("๐ŸชŸ Stash window positioned on screen: \(screenToDisplayOn.customLocalizedName) at {\(targetX), \(targetY)}") - } else { - newWindow.center() // Fallback - print("๐ŸชŸ Stash window centered (could not determine target screen).") - } - - newWindow.delegate = self - let newWindowController = NSWindowController(window: newWindow) - newWindowController.showWindow(self) - self.stashWindowController = newWindowController - self.activeStashImageStore = imageStore - - if SettingsManager.shared.stashAlwaysOnTop { - newWindow.level = .floating - print("๐Ÿ” New empty stash window created with always on top") - } else { - newWindow.level = .normal - print("๐Ÿ“‹ New empty stash window created with normal level") - } - - newWindow.makeKeyAndOrderFront(nil as Any?) - NSApp.activate(ignoringOtherApps: true) - - // ๐ŸŽฏ NIEUW: Subtiele shake animatie voor nieuwe stash windows - print("๐ŸŽ‰ Adding shake animation to new stash window") - addSubtleShakeAnimation(to: newWindow) - } - - func setTempFileURL(_ url: URL?) { - // DEBUG LOG - if let newURL = url { - print("๐Ÿ”„ setTempFileURL: New URL = \(newURL.path)") - } else { - print("๐Ÿ”„ setTempFileURL: Clearing URL (nil)") - } - - // AANGEPAST: Behoud alle screenshots, laat cache retention systeem de cleanup doen - // Als url is nil, clear alleen de tempURL referentie, verwijder het bestand NIET - if url == nil { - if let oldURL = tempURL { - print("โ„น๏ธ Clearing tempURL reference to: \(oldURL.lastPathComponent) (file kept for cache retention)") - } - tempURL = nil - print("โ„น๏ธ Cleared tempURL reference (screenshot preserved).") - } else if let newURL = url { - // Geen cleanup van oude files - alle screenshots blijven bestaan voor cache retention - if let oldURL = tempURL, oldURL != newURL { - print("๐Ÿ“ธ Previous screenshot preserved: \(oldURL.lastPathComponent)") - } - tempURL = newURL - print("โ„น๏ธ Updated tempURL to: \(newURL.lastPathComponent)") - } - } - - // MOVED: findFilenameLabel function moved to PreviewManagerDelegate extension - - func saveImageDataToURL(_ url: URL, from sourceURL: URL) -> Bool { - print("๐Ÿ’พ Attempting to save data from \(sourceURL.lastPathComponent) to \(url.lastPathComponent)") - do { - let imageData = try Data(contentsOf: sourceURL) - try imageData.write(to: url) - print("๐Ÿ“Š Saved PNG data size: \(imageData.count) bytes") - return true - } catch { - print("โŒ Error reading from \(sourceURL.path) or writing to \(url.path): \(error)") - return false - } - } - - // Implementatie van RenameActionHandlerDelegate methoden - func getActivePreviewWindow() -> NSWindow? { - // NIEUW: Delegate to PreviewManager - return previewManager.getActivePreviewWindow() - } - - // MARK: - Legacy code cleanup - CancelRemoveButtonsView methods removed - // These methods were replaced by the grid-based Cancel/Remove system in GridComponents - - - - func renameActionHandler(_ handler: RenameActionHandler, didRenameFileFrom oldURL: URL, to newURL: URL) { - print("Delegate: File was renamed from \(oldURL) to \(newURL)") - if self.tempURL == oldURL { - self.setTempFileURL(newURL) - if let previewWindow = getActivePreviewWindow(), let label = self.findFilenameLabel(in: previewWindow) { - label.stringValue = newURL.lastPathComponent - label.toolTip = newURL.lastPathComponent - } - } else { - // Zoek in stash - if let store = self.activeStashImageStore { - if let idx = store.images.firstIndex(where: { $0.fileURL == oldURL }) { - store.images[idx].fileURL = newURL - store.images[idx].customName = newURL.lastPathComponent - print("โœ… Stash image updated to new name \(newURL.lastPathComponent)") - } - } - print("Info: Renamed file was not the active tempURL.") - } - } - - // TOEGEVOEGD: Vereiste methode voor RenameActionHandlerDelegate - func getScreenshotFolder() -> String? { - return SettingsManager.shared.screenshotFolder - } - - // REQUIRED: findFilenameLabel method for RenameActionHandlerDelegate protocol - func findFilenameLabel(in window: NSWindow?) -> NSTextField? { - guard let targetWindow = window else { return nil } - return targetWindow.contentView?.viewWithTag(11001) as? NSTextField - } - - // NIEUW: Hide grid delegate method - func hideGrid() { - print("๐Ÿ”ถ RenameActionHandlerDelegate: Hiding grid after rename action") - // GEWIJZIGD: monitorForReappear naar false zodat de grid NIET LATER KAN TERUGKEREN - gridViewManager?.hideGrid(monitorForReappear: false) - } - - // NIEUW: Disable grid monitoring delegate method - func disableGridMonitoring() { - print("๐Ÿ”ถ RenameActionHandlerDelegate: Disabling grid monitoring during rename") - gridViewManager?.disableMonitoring() - } - - // NIEUW: Enable grid monitoring delegate method - func enableGridMonitoring() { - print("๐Ÿ”ถ RenameActionHandlerDelegate: Enabling grid monitoring after rename") - gridViewManager?.enableMonitoring() - } - - // HERSTEL / VOEG DEZE IMPLEMENTATIE TOE BINNEN DE KLASSE - func gridViewManager(_ manager: GridViewManager, didDropImage imageURL: URL, ontoCell cellIndex: Int, at dropPoint: NSPoint) { - print("โœ… ScreenshotApp: Received delegate call for drop on cell \(cellIndex)") - print("๐Ÿ–ผ๏ธ Dropped image \(imageURL.path) onto cell \(cellIndex)") - self.didGridHandleDrop = true - - // VERWIJDERD: Oude logica voor het direct verbergen van de grid. - // De GridActionManager is nu verantwoordelijk voor het correct verbergen van de grid - // na het afhandelen van de actie en de FeedbackBubblePanel (indien van toepassing). - // De RenameActionHandler beheert het ook zelf via zijn delegate. - - // NIEUW: Delegate to GridActionManager - gridActionManager.handleGridAction( - imageURL: imageURL, - cellIndex: cellIndex, - dropPoint: dropPoint, - gridWindow: manager.gridWindow - ) { [weak self] actionCompletedSuccessfully, wasSaveToFolder, isStashAction in - guard let self = self else { return } - self.gridActionManager.handleActionCompletion( - actionCompletedSuccessfully: actionCompletedSuccessfully, - wasSaveToFolder: wasSaveToFolder, - isStashAction: isStashAction - ) - } - } - - // De @objc func saveImage() methode moet hier ook staan... - @objc func saveImage() { - // ... volledige implementatie van saveImage ... - } - - // Nieuwe methode om selectie veilig te annuleren - func cancelSelection(overlay: OverlayWindow?) { - print("๐Ÿ”ต Canceling selection...") - CGDisplayShowCursor(CGMainDisplayID()) - - DispatchQueue.main.async { - print("๐Ÿ”ต Ordering out overlay asynchronously from cancelSelection...") - overlay?.orderOut(nil as Any?) - // Zorg ervoor dat de referentie wordt opgeruimd als deze overlay de actieve was - if self.activeOverlayWindow === overlay { - self.activeOverlayWindow = nil - } - } - } - - // Nieuwe functie voor visuele feedback - func flashPreviewBorder() { - // NIEUW: Delegate to PreviewManager - previewManager.flashPreviewBorder() - } - - @objc func saveFromPreview(_ sender: Any) { - _saveFromPreview(sender) - } - - // HERSTELDE/TOEGEVOEGDE FUNCTIE - @objc func openScreenshotFolder() { - _openScreenshotFolder() - } - - // MARK: - Background Remove Helper - - // REMOVED: Legacy BGR window system - now uses thumbnail-based BGR workflow - - // ๐ŸŽจ NEW: Background removal thumbnail workflow - func showBackgroundRemovalThumbnail(with image: NSImage, originalURL: URL) { - print("๐ŸŽจ ScreenshotApp: Starting BGR thumbnail workflow") - - // Always hide the grid when starting BGR workflow - gridViewManager?.hideGrid(monitorForReappear: false) - - // ๐Ÿ”ง CRITICAL FIX: Do NOT close existing preview here! - // Let the normal drag completion logic in DraggableImageView handle preview closing - // based on the user's "close thumbnail after dragging" setting. - print("๐Ÿ”„ BGR: Starting BGR workflow without closing existing preview (handled by drag completion)") - - // Use PreviewManager's new BGR mode with original URL - previewManager?.showBackgroundRemovalPreview(originalImage: image, originalURL: originalURL) - } - - // MARK: - OCR & Toast Helpers - - private func performOcrAndCopy(from imageURL: URL, dropPoint: NSPoint, gridWindow: NSWindow?) { - // MOVED TO GridActionManager.swift - performOcrAndCopy functionality - } - - private func showToast(message: String, near point: NSPoint, gridWindow: NSWindow?) { - // MOVED TO GridActionManager.swift - showToast functionality - } - - // Helper to copy image to clipboard and show toast - private func copyImageToClipboard(from imageURL: URL, dropPoint: NSPoint, gridWindow: NSWindow?) { - // MOVED TO GridActionManager.swift - copyImageToClipboard functionality - } - - // NIEUW: Functie om automatisch opstarten te regelen - func toggleLaunchAtLogin(shouldLaunch: Bool) { - if #available(macOS 13.0, *) { - // Voor macOS 13+ gebruiken we SMAppService.mainApp - // Dit werkt direct voor de main app zonder extra configuratie - let service = SMAppService.mainApp - do { - if shouldLaunch { - // Controleer eerst de huidige status - let currentStatus = service.status - print("๐Ÿ” LOGIN: Current SMAppService.mainApp status: \(currentStatus)") - - switch currentStatus { - case .notRegistered: - try service.register() - print("โœ… LOGIN: App successfully registered to launch at login via SMAppService.mainApp") - // Check status na registratie - let newStatus = service.status - print("๐Ÿ” LOGIN: Status after registration: \(newStatus)") - // Geen popup meer - gebruiker krijgt macOS native bevestiging - case .enabled: - print("โ„น๏ธ LOGIN: App is already enabled for launch at login") - case .requiresApproval: - print("โš ๏ธ LOGIN: Launch at login requires user approval in System Settings") - // Geen popup meer - macOS toont native bevestiging automatisch - case .notFound: - print("โŒ LOGIN: App service not found - SMAppService.mainApp failed") - // Fallback naar legacy methode - print("๐Ÿ”„ LOGIN: Falling back to legacy method for macOS 13+") - toggleLaunchAtLoginLegacy(shouldLaunch: shouldLaunch) - return - @unknown default: - print("โš ๏ธ LOGIN: Unknown SMAppService status: \(currentStatus)") - } - } else { - // Uitschakelen - let currentStatus = service.status - if currentStatus == .enabled || currentStatus == .requiresApproval { - try service.unregister() - print("โœ… LOGIN: App unregistered from launch at login (SMAppService.mainApp)") - } else { - print("โ„น๏ธ LOGIN: App was not registered for launch at login (status: \(currentStatus))") - } - } - } catch { - print("โŒ LOGIN: Error with SMAppService.mainApp: \(error.localizedDescription)") - print("๐Ÿ”„ LOGIN: Falling back to legacy method") - toggleLaunchAtLoginLegacy(shouldLaunch: shouldLaunch) - return - } - } else { - // Voor macOS < 13 - print("โš ๏ธ LOGIN: macOS version < 13.0 detected, using legacy method") - toggleLaunchAtLoginLegacy(shouldLaunch: shouldLaunch) - } - } - - // Legacy methode voor macOS < 13 en fallback - private func toggleLaunchAtLoginLegacy(shouldLaunch: Bool) { - // Voor macOS < 13 of als SMAppService.mainApp faalt - print("๐Ÿ”„ LOGIN: Using legacy approach - LSSharedFileList") - - // Probeer LSSharedFileList methode (werkt tot macOS 13) - if #available(macOS 10.11, *) { - if toggleLaunchAtLoginUsingLSSharedFileList(shouldLaunch: shouldLaunch) { - print("โœ… LOGIN: Successfully used LSSharedFileList method") - return - } - } - - // Als alles faalt, toon instructies aan gebruiker - DispatchQueue.main.async { - let alert = NSAlert() - alert.messageText = "Manual Setup Required" - if shouldLaunch { - alert.informativeText = "To enable ShotScreen to start at login:\n\n1. Open System Settings/Preferences\n2. Go to General โ†’ Login Items (or Users & Groups โ†’ Login Items)\n3. Click + and add ShotScreen from Applications" - } else { - alert.informativeText = "To disable ShotScreen from starting at login:\n\n1. Open System Settings/Preferences\n2. Go to General โ†’ Login Items (or Users & Groups โ†’ Login Items)\n3. Select ShotScreen and click - or toggle it off" - } - alert.addButton(withTitle: "Open System Settings") - alert.addButton(withTitle: "OK") - - let response = alert.runModal() - if response == .alertFirstButtonReturn { - // Open System Settings/Preferences - if #available(macOS 13.0, *) { - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.users?LoginItems") { - NSWorkspace.shared.open(url) - } - } else { - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.users") { - NSWorkspace.shared.open(url) - } - } - } - } - - // Reset de setting omdat gebruiker handmatig moet handelen - DispatchQueue.main.async { - SettingsManager.shared.startAppOnLogin = false - } - } - - // Hulpfunctie voor LSSharedFileList (legacy) - alleen voor testen - @available(macOS 10.11, *) - private func toggleLaunchAtLoginUsingLSSharedFileList(shouldLaunch: Bool) -> Bool { - // Deze API's zijn deprecated vanaf 10.11, dus gebruiken we alleen SMAppService.mainApp - print("โš ๏ธ LOGIN: LSSharedFileList APIs are deprecated, falling back to manual setup") - return false - } - - // Toon instructies voor gebruiker om launch at login goed te keuren - private func showLoginItemApprovalAlert() { - DispatchQueue.main.async { - let alert = NSAlert() - alert.messageText = "Launch at Login Requires Approval" - alert.informativeText = "To enable ShotScreen to start automatically at login:\n\n1. Open System Settings\n2. Go to General โ†’ Login Items\n3. Find ShotScreen and enable it" - alert.addButton(withTitle: "Open System Settings") - alert.addButton(withTitle: "Cancel") - - let response = alert.runModal() - if response == .alertFirstButtonReturn { - // Open System Settings to Login Items - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.users?LoginItems") { - NSWorkspace.shared.open(url) - } - } - } - } - - // NIEUW: Functie om direct op te slaan zonder interactie - func saveDirectlyToConfiguredFolder(isCalledFromPreviewButton: Bool = false) { - print("๐Ÿ’พ Opslaan naar geconfigureerde map gestart. Oorsprong: \(isCalledFromPreviewButton ? "Preview Knop" : "Automatisch")") - guard let sourceURL = self.tempURL else { - print("โŒ Opslaan Mislukt: Geen tijdelijk screenshot bestand URL beschikbaar.") - return - } - - guard let destinationFolderStr = SettingsManager.shared.screenshotFolder, - !destinationFolderStr.isEmpty else { - print("โŒ Opslaan Mislukt: Standaard opslagmap niet ingesteld.") - if let window = previewManager.getActivePreviewWindow() { // Toon alleen alert als er een UI context is - DispatchQueue.main.async { - let alert = NSAlert() - alert.messageText = "Save Folder Not Set" - alert.informativeText = "Please set a default save folder in Settings before using the save button." - alert.addButton(withTitle: "OK") - alert.beginSheetModal(for: window, completionHandler: nil) - } - } - return - } - - let fileManager = FileManager.default - let destinationFolderURL = URL(fileURLWithPath: destinationFolderStr) - - // 1. Bepaal basisnaam en extensie van de sourceURL - var baseName = sourceURL.deletingPathExtension().lastPathComponent - let ext = sourceURL.pathExtension.isEmpty ? "png" : sourceURL.pathExtension // Behoud en gebruik 'ext' - - // 2. Verwijder eventueel bestaande "_getal" suffix van de baseName - // Dit voorkomt namen zoals "bestand_1_1.png" als "bestand_1.png" al bestond. - if let regex = try? NSRegularExpression(pattern: "_(\\d+)$"), - let match = regex.firstMatch(in: baseName, range: NSRange(baseName.startIndex..., in: baseName)), - match.numberOfRanges > 1 { - let numberRange = Range(match.range(at: 1), in: baseName)! - if let _ = Int(baseName[numberRange]) { // Controleer of het echt een getal is - baseName = String(baseName[.. 1 { - let numberRange = Range(match.range(at: 1), in: baseName)! - if let _ = Int(baseName[numberRange]) { - baseName = String(baseName[.. NSWindow? { - // MOVED TO PreviewManager.swift - showFloatingThumbnail functionality - return nil - } - - // MARK: - Screen Selection for Thumbnail - func getTargetScreenForThumbnail() -> NSScreen? { - // MOVED TO PreviewManager.swift - getTargetScreenForThumbnail functionality - return previewManager.getTargetScreenForThumbnail() - } - - // HERNOEM EN PAS AAN: showFloatingThumbnail naar createNewThumbnailWindow - // MOVED TO PreviewManager.swift - createNewThumbnailWindow functionality - - // MARK: - Multi-Monitor Functions - // Multi-monitor functionality has been moved to MultiMonitorSystem.swift - - // MARK: - Clean Desktop Functions - // VERWIJDERD: Desktop cleaning functions - feature has been disabled - - // Cleanup function from MultiMonitorSystem.swift is used instead - - // MARK: - GridActionManagerDelegate Implementation - func disableMonitoring() { // Renamed from gridViewManagerDisableMonitoring - gridViewManager?.disableMonitoring() - } - - func enableMonitoring() { // Renamed from gridViewManagerEnableMonitoring - gridViewManager?.enableMonitoring() - } - - func gridViewManagerHideGrid(monitorForReappear: Bool) { - gridViewManager?.hideGrid(monitorForReappear: monitorForReappear) - } - - func getGridCurrentFrame() -> NSRect? { - return gridViewManager?.gridWindow?.frame - } - - // NIEUW: Methode om de "alle schermen" toggle modus te beheren - func toggleAllScreensCaptureMode() { - isAllScreensCaptureToggledOn.toggle() - print("๐Ÿ”„ All Screens Capture Toggled: \(isAllScreensCaptureToggledOn ? "ON" : "OFF")") - updateAllScreensModeNotifier() - } - - // MARK: - Public Methods for Stash Preview Positioning - - /// Returns the target screen for stash preview positioning (same as main thumbnail) - func getTargetScreenForStashPreview() -> NSScreen? { - return previewManager.getTargetScreenForThumbnail() - } - - // MARK: - Private stash implementation helpers - - // NIEUW: Subtiele shake animatie voor stash window - private func addSubtleShakeAnimation(to window: NSWindow) { - print("๐Ÿ”ฅ STARTING PROVEN CAKeyframeAnimation shake for stash window") - - // PROVEN WORKING PARAMETERS from Eric Dolecki blog - let numberOfShakes = 4 - let durationOfShake = 0.3 - let vigourOfShake: CGFloat = 0.03 // 3% of window width - more visible - let frame = window.frame - - print("๐Ÿ”ฅ Shake parameters: shakes=\(numberOfShakes), duration=\(durationOfShake)s, vigour=\(vigourOfShake)") - print("๐Ÿ”ฅ Window frame: \(frame)") - - // Create CAKeyframeAnimation using PROVEN technique - let shakeAnimation = CAKeyframeAnimation() - - // Create shake path with left-right movement - let shakePath = CGMutablePath() - shakePath.move(to: CGPoint(x: frame.minX, y: frame.minY)) - - for _ in 0.. 0 { - if persistentStashImageStore == nil { - persistentStashImageStore = GalleryImageStore() - } - - // Copy all images to persistent store - persistentStashImageStore!.images = currentStore.images - print("๐Ÿ”ฅ PERSISTENT STASH: Saved \(currentStore.images.count) images for next session") - } else { - print("๐Ÿ”ฅ PERSISTENT STASH: No images to save") - } - } else { - print("๐Ÿ”ฅ PERSISTENT STASH: Persistent stash disabled - clearing all images") - persistentStashImageStore = nil - } - - stashWindowController = nil - activeStashImageStore = nil - } - - // Check voor settings window - if let settingsWin = activeSettingsWindow, settingsWin == closingWindow { - print("Settings window closed. Clearing reference.") - activeSettingsWindow = nil - } - } -} - -// MARK: - MenuManagerDelegate Implementation -extension ScreenshotApp { - func triggerScreenshot() { - activateMultiMonitorSelection() - } - - func triggerWholeScreenCapture() { - print("๐Ÿ–ฅ๏ธ Triggering whole screen capture under cursor from menu!") - - // Use existing function that handles everything correctly - captureWholeScreenUnderCursor() - } - - func triggerNativeWindowCapture() { - print("๐ŸชŸ Triggering native window capture - direct window selection!") - - // Create temporary file for screenshot - let tempDirectory = FileManager.default.temporaryDirectory - let tempFileName = "ShotScreen_window_\(UUID().uuidString).png" - let tempFileURL = tempDirectory.appendingPathComponent(tempFileName) - - print("๐Ÿ“„ Using temporary file: \(tempFileURL.path)") - - // Start screencapture to file (NO clipboard) - let task = Process() - task.launchPath = "/usr/sbin/screencapture" - task.arguments = [ - "-iw", // Interactive WINDOW selection (direct) - "-x", // Do not play sounds - tempFileURL.path // Save to file - ] - - do { - try task.run() - - // Wait for screencapture completion in background - DispatchQueue.global(qos: .userInitiated).async { [weak self] in - task.waitUntilExit() - - DispatchQueue.main.async { [weak self] in - guard let self = self else { return } - - // Check if file was created (indicates successful capture) - if FileManager.default.fileExists(atPath: tempFileURL.path) { - // Extra check: Is it a valid image file? - if let image = NSImage(contentsOf: tempFileURL), - image.size.width > 0 && image.size.height > 0 { - print("โœ… Valid window screenshot saved to file - processing...") - self.processCapture(image: image) - - // Clean up temporary file - try? FileManager.default.removeItem(at: tempFileURL) - print("๐Ÿ—‘๏ธ Temporary window screenshot file cleaned up") - } else { - print("๐Ÿšซ Invalid/empty image file - right-click cancelled") - try? FileManager.default.removeItem(at: tempFileURL) - } - } else { - print("๐Ÿšซ No screenshot file - ESC or other cancellation") - } - } - } - } catch { - print("โŒ Failed to start window capture: \(error)") - } - } - - func triggerAllScreensCapture() { - // ๐Ÿ”ง FIX: Menu item bypasses desktop filtering - direct native capture - print("๐Ÿ“‹ Menu: Capture All Screens - using direct native capture (bypasses desktop filtering)") - captureAllScreensNative() - } - - func openSettings() { - openSettings(nil) - } - - func showStash() { - showStash(nil) - } - - func resetFirstLaunchWizard() { - self.resetFirstLaunchWizardInternal() - } - - func toggleDesktopIcons() { - // Toggle the setting - SettingsManager.shared.hideDesktopIconsDuringScreenshot.toggle() - - // Update the menu item to reflect the new state - menuManager?.refreshDesktopIconsMenuItem() - - // Log the change - let newState = SettingsManager.shared.hideDesktopIconsDuringScreenshot - print("๐Ÿ–ฅ๏ธ Desktop icons during screenshots: \(newState ? "HIDDEN" : "VISIBLE")") - - // DISABLED: Visual feedback causes crashes due to complex animation closures - // showDesktopIconsToggleFeedback(hidden: newState) - } - - func toggleDesktopWidgets() { - // Toggle the setting - SettingsManager.shared.hideDesktopWidgetsDuringScreenshot.toggle() - - // Update the menu item to reflect the new state - menuManager?.refreshDesktopWidgetsMenuItem() - - // Log the change - let newState = SettingsManager.shared.hideDesktopWidgetsDuringScreenshot - print("๐Ÿ“ฑ Desktop widgets during screenshots: \(newState ? "HIDDEN" : "VISIBLE")") - } - - // MARK: - License Management - private func initializeLicenseSystem() { - print("๐Ÿ” LICENSE: Initializing license system...") - - // Initialize license manager which will check status on startup - _ = LicenseManager.shared - - // Add observer for license status changes - NotificationCenter.default.addObserver( - self, - selector: #selector(handleLicenseStatusChanged), - name: .licenseStatusChanged, - object: nil - ) - - // Start observing license status - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { - self.checkLicenseStatusAndEnforcePolicy() - } - } - - @objc private func handleLicenseStatusChanged() { - DispatchQueue.main.async { - self.checkLicenseStatusAndEnforcePolicy() - } - } - - private func checkLicenseStatusAndEnforcePolicy() { - let licenseManager = LicenseManager.shared - - print("๐Ÿ” LICENSE: Checking current license status...") - - switch licenseManager.licenseStatus { - case .checking: - print("๐Ÿ” LICENSE: Status checking in progress...") - // Wait a bit and check again - DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { - self.checkLicenseStatusAndEnforcePolicy() - } - - case .licensed(let userName, let email): - print("โœ… LICENSE: Valid production license found for \(userName) (\(email))") - // App can function normally - enableAppFunctionality() - - case .testLicense(let userName, let email): - print("๐Ÿงช LICENSE: Valid test license found for \(userName) (\(email))") - print("๐Ÿงช LICENSE: Test license detected - app functionality enabled for development") - // App can function normally (same as production license) - enableAppFunctionality() - - case .trial(let daysLeft): - print("โฐ LICENSE: Trial active with \(daysLeft) days remaining") - if daysLeft <= 0 { - print("โŒ LICENSE: Trial has expired!") - showTrialExpiredBlocker() - } else { - // Show trial reminder for last 2 days - if daysLeft <= 2 { - showTrialReminder(daysLeft: daysLeft) - } - enableAppFunctionality() - } - - case .expired: - print("โŒ LICENSE: Trial has expired!") - showTrialExpiredBlocker() - - case .invalid: - print("โŒ LICENSE: Invalid license detected!") - showInvalidLicenseBlocker() - } - } - - private func showTrialReminder(daysLeft: Int) { - let alert = NSAlert() - alert.alertStyle = .informational - alert.messageText = "Trial Reminder" - alert.informativeText = """ - Your ShotScreen trial expires in \(daysLeft) day\(daysLeft == 1 ? "" : "s"). - - Purchase a license to continue using ShotScreen after the trial period. - """ - alert.addButton(withTitle: "Purchase License") - alert.addButton(withTitle: "Enter License Key") - alert.addButton(withTitle: "Continue Trial") - - let response = alert.runModal() - - switch response { - case .alertFirstButtonReturn: - openPurchaseURL() - case .alertSecondButtonReturn: - showLicenseEntryDialog() - default: - break - } - } - - private func showTrialExpiredBlocker() { - // Create and show a blocking window that prevents app usage - let expiredWindow = createTrialExpiredWindow() - expiredWindow.makeKeyAndOrderFront(nil) - - // Disable all app functionality except license entry - disableAppFunctionality() - } - - private func showInvalidLicenseBlocker() { - let alert = NSAlert() - alert.alertStyle = .critical - alert.messageText = "Invalid License" - alert.informativeText = """ - Your license is invalid or has been revoked. - - Please contact support or purchase a new license to continue using ShotScreen. - """ - alert.addButton(withTitle: "Purchase License") - alert.addButton(withTitle: "Enter License Key") - alert.addButton(withTitle: "Quit") - - let response = alert.runModal() - - switch response { - case .alertFirstButtonReturn: - openPurchaseURL() - case .alertSecondButtonReturn: - showLicenseEntryDialog() - default: - NSApplication.shared.terminate(nil) - } - } - - private func createTrialExpiredWindow() -> NSWindow { - let window = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 400, height: 350), - styleMask: [.titled], - backing: .buffered, - defer: false - ) - - window.title = "ShotScreen - Trial Expired" - window.center() - window.level = .modalPanel - window.isReleasedWhenClosed = false - - // Make window non-closable and non-resizable - window.standardWindowButton(.closeButton)?.isHidden = true - window.standardWindowButton(.miniaturizeButton)?.isHidden = true - window.standardWindowButton(.zoomButton)?.isHidden = true - - let trialExpiredView = TrialExpiredView() - let hostingView = NSHostingView(rootView: trialExpiredView) - window.contentView = hostingView - - return window - } - - private func disableAppFunctionality() { - // Disable hotkey - hotKey = nil - - // Close any open windows - if let preview = activePreviewWindow { - preview.close() - } - - if let stash = stashWindowController?.window { - stash.close() - } - - // Hide menu or disable menu items - menuManager?.setAppEnabled(false) - - print("๐Ÿ”’ LICENSE: App functionality disabled due to expired/invalid license") - } - - private func enableAppFunctionality() { - // Re-enable hotkey - setupHotKey() - - // Re-enable menu - menuManager?.setAppEnabled(true) - - print("๐Ÿ”“ LICENSE: App functionality enabled with valid license") - } - - private func showLicenseEntryDialog() { - LicenseManager.shared.showLicenseEntryDialog() - } - - private func openPurchaseURL() { - // ShotScreen Gumroad license product - if let url = URL(string: "https://roodenrijs.gumroad.com/l/uxexr") { - NSWorkspace.shared.open(url) - } - } - -} - -// MARK: - PreviewManagerDelegate Implementation -extension ScreenshotApp { - func getLastImage() -> NSImage? { - return lastImage - } - - func setLastImage(_ image: NSImage) { - lastImage = image - } - - // ๐ŸŽฏ NEW: Create backup for thumbnail restoration - private func createImageBackup(image: NSImage, tempURL: URL) { - // Store the backup image and path - self.backupImage = image - self.backupImagePath = tempURL.path - - // ๐ŸŽฏ NEW: Create a persistent backup file that won't be deleted - let backupDirectory = thumbnailDirectory.appendingPathComponent("thumbnail_restoration") - do { - // Always ensure the directory exists (recreate if user deleted it) - try FileManager.default.createDirectory(at: backupDirectory, withIntermediateDirectories: true, attributes: nil) - let backupFileURL = backupDirectory.appendingPathComponent("latest_backup.png") - - if let pngData = createPngData(from: image) { - try pngData.write(to: backupFileURL) - self.backupImagePath = backupFileURL.path - print("๐Ÿ’พ Thumbnail restoration backup created: \(tempURL.lastPathComponent) โ†’ saved to \(backupFileURL.lastPathComponent)") - } - } catch { - print("โŒ Failed to create thumbnail restoration backup file: \(error)") - // Fall back to original path (might be deleted) - self.backupImagePath = tempURL.path - print("๐Ÿ’พ Thumbnail restoration backup created: \(tempURL.lastPathComponent) (fallback)") - } - } - - // ๐ŸŽฏ NEW: Restore thumbnail after settings changes that may have closed it - func restoreCurrentThumbnailIfNeeded() { - // Check if there's a backup available and no active thumbnail - guard let backupImg = backupImage, - let backupPath = backupImagePath, - previewManager.getActivePreviewWindow() == nil else { - print("๐Ÿ”„ No need to restore thumbnail - either no backup available or thumbnail already open") - return - } - - print("๐Ÿ”„ Restoring thumbnail from restoration backup: \(URL(fileURLWithPath: backupPath).lastPathComponent)") - - // Check if backup file still exists before restoring - if !FileManager.default.fileExists(atPath: backupPath) { - print("โŒ Thumbnail restoration backup file no longer exists at: \(backupPath)") - clearImageBackup() - return - } - - // Small delay to ensure any settings UI updates are complete - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in - guard let self = self else { return } - - // Create a new temp file for the restored image - let newTempURL = self.createTempUrl() - - do { - if let pngData = self.createPngData(from: backupImg) { - try pngData.write(to: newTempURL) - - // Restore the state - self.lastImage = backupImg - self.tempURL = newTempURL - - // Show the thumbnail again with the backup image - self.showPreview(image: backupImg) - - print("โœ… Thumbnail restored from restoration backup successfully: \(newTempURL.lastPathComponent)") - } else { - print("โŒ Failed to create PNG data for thumbnail restoration backup restore") - } - } catch { - print("โŒ Failed to write restored thumbnail restoration backup file: \(error)") - } - } - } - - func clearTempFile() { - setTempFileURL(nil) - // ๐ŸŽฏ NEW: Don't clear thumbnail restoration backup here - only clear when explicitly needed - // clearImageBackup() - Removed: Let restore function handle backup cleanup - } - - // ๐ŸŽฏ NEW: Clear thumbnail restoration backup system - private func clearImageBackup() { - // Clean up backup file if it exists - if let backupPath = backupImagePath { - let backupURL = URL(fileURLWithPath: backupPath) - if backupURL.lastPathComponent == "latest_backup.png" { - try? FileManager.default.removeItem(at: backupURL) - print("๐Ÿ—‘๏ธ Thumbnail restoration backup file deleted: \(backupURL.lastPathComponent)") - } - } - - backupImage = nil - backupImagePath = nil - print("๐Ÿ—‘๏ธ Thumbnail restoration backup cleared") - } - - // ๐ŸŽฏ NEW: Clear thumbnail restoration backup only in specific cases - func clearBackupAfterNewScreenshot() { - clearImageBackup() - print("๐Ÿ—‘๏ธ Thumbnail restoration backup cleared after new screenshot") - } - - func getTempURL() -> URL? { - return tempURL - } - - // MARK: - Private implementations to avoid recursion - private func _openScreenshotFolder() { - if let path = SettingsManager.shared.screenshotFolder, !path.isEmpty { - print("Opening folder: \(path)") - let url = URL(fileURLWithPath: path) - NSWorkspace.shared.open(url) - } else { - print("Screenshot folder not set.") - let alert = NSAlert() - alert.messageText = "Screenshot Folder Not Set" - alert.informativeText = "Please set a default screenshot folder in Settings." - alert.addButton(withTitle: "OK") - alert.addButton(withTitle: "Open Settings") - let response = alert.runModal() - if response == .alertSecondButtonReturn { - _openSettings(nil) - } - } - } - - private func _openSettings(_ sender: Any?) { - renameActionHandler.closeRenamePanelAndCleanup() - - if let existingWindow = activeSettingsWindow, existingWindow.isVisible { - print("Settings window already open. Bringing to front.") - existingWindow.level = NSWindow.Level.floating - existingWindow.orderFrontRegardless() - existingWindow.makeKeyAndOrderFront(nil as Any?) - NSApp.activate(ignoringOtherApps: true) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - existingWindow.level = NSWindow.Level.normal - } - return - } - - print("Opening new settings window.") - let currentFolder = SettingsManager.shared.screenshotFolder - let url = currentFolder != nil ? URL(fileURLWithPath: currentFolder!) : nil - let timerValue = SettingsManager.shared.thumbnailTimer - - let newSettingsWindow = SettingsWindow(currentFolder: url, timerValue: timerValue, delegate: self) - self.activeSettingsWindow = newSettingsWindow - - newSettingsWindow.orderFront(nil as Any?) - newSettingsWindow.makeKeyAndOrderFront(nil as Any?) - NSApp.activate(ignoringOtherApps: true) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - newSettingsWindow.orderFront(nil as Any?) - } - } - - private func _saveFromPreview(_ sender: Any) { - print("๐Ÿ’พ Save From Preview button clicked.") - - // ๐ŸŽจ NEW: Check if we're in BGR mode and use the appropriate URL - if let previewMgr = previewManager, previewMgr.isBackgroundRemovalMode { - let bgrSourceURL: URL? - - if previewMgr.isShowingProcessedImage { - bgrSourceURL = previewMgr.processedImageURL - print("๐Ÿ’พ BGR Mode: Saving processed image from \(bgrSourceURL?.path ?? "nil")") - } else { - bgrSourceURL = previewMgr.originalImageURL - print("๐Ÿ’พ BGR Mode: Saving original image from \(bgrSourceURL?.path ?? "nil")") - } - - if let sourceURL = bgrSourceURL { - saveBGRImageToConfiguredFolder(sourceURL: sourceURL, isCalledFromPreviewButton: true) - } else { - print("โŒ BGR Mode: No valid source URL available for save") - } - } else { - // Normal mode - use existing implementation - saveDirectlyToConfiguredFolder(isCalledFromPreviewButton: true) - } - } - - // MARK: - Desktop Icons Toggle Feedback - // DISABLED: These functions cause crashes due to complex animation and window management - /* - private func showDesktopIconsToggleFeedback(hidden: Bool) { - // Create a simple notification-style feedback window - let message = hidden ? "Desktop icons will be hidden during screenshots" : "Desktop icons will be visible during screenshots" - let icon = hidden ? "folder.badge.minus" : "folder.badge.plus" - - // Find the screen where the mouse is for positioning - let mouseLocation = NSEvent.mouseLocation - guard let screen = NSScreen.screens.first(where: { $0.frame.contains(mouseLocation) }) else { return } - - // Create feedback window - let feedbackWindow = createFeedbackWindow(message: message, icon: icon, on: screen) - - // Show and auto-hide the window - feedbackWindow.makeKeyAndOrderFront(nil as Any?) - - // Enhanced Animation with weak self reference and completion handler for safety - var animationWorkItem: DispatchWorkItem? - let workItem = DispatchWorkItem { [weak feedbackWindow, weak self] in - guard let self = self, let strongFeedbackWindow = feedbackWindow else { return } - - NSAnimationContext.runAnimationGroup({ context in - context.duration = 0.5 // Duration of the fade-out animation - context.timingFunction = CAMediaTimingFunction(name: .easeIn) - strongFeedbackWindow.animator().alphaValue = 0 - }, completionHandler: { - strongFeedbackWindow.close() - // Ensure the work item is nilled out after execution to break potential retain cycles - if animationWorkItem === DispatchWorkItem.cancelled || animationWorkItem?.isCancelled == true { - // Already cancelled, do nothing further - } else { - animationWorkItem = nil // Break cycle after completion - } - }) - } - animationWorkItem = workItem // Assign to the tracking variable - - DispatchQueue.main.asyncAfter(deadline: .now() + 2, execute: workItem) - } - - private func createFeedbackWindow(message: String, icon: String, on screen: NSScreen) -> NSWindow { - let window = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 300, height: 60), // Adjusted size - styleMask: [.borderless], - backing: .buffered, - defer: false - ) - - window.isReleasedWhenClosed = true // Changed to true - window.level = .floating // Keep on top - window.backgroundColor = .clear - window.isOpaque = false - window.hasShadow = false // No shadow for this simple notification - window.isMovableByWindowBackground = false - window.ignoresMouseEvents = true // Click-through - - // Position the window at the center of the specified screen - let screenRect = screen.visibleFrame - let windowRect = window.frame - let xPos = screenRect.origin.x + (screenRect.width - windowRect.width) / 2 - let yPos = screenRect.origin.y + (screenRect.height - windowRect.height) / 2 - window.setFrameOrigin(NSPoint(x: xPos, y: yPos)) - - // Content View with rounded corners and blur - let contentView = NSView() - contentView.wantsLayer = true - contentView.layer?.cornerRadius = 12 // Rounded corners - - let blurView = NSVisualEffectView() - blurView.material = .hudWindow // Or another appropriate material - blurView.blendingMode = .behindWindow - blurView.state = .active - blurView.frame = contentView.bounds - blurView.autoresizingMask = [.width, .height] - contentView.addSubview(blurView) - - // StackView for Icon and Text - let stackView = NSStackView() - stackView.orientation = .horizontal - stackView.alignment = .centerY - stackView.spacing = 10 - stackView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(stackView) - - // Icon - if let nsIcon = NSImage(systemSymbolName: icon, accessibilityDescription: nil) { - let imageView = NSImageView(image: nsIcon) - imageView.symbolConfiguration = .init(pointSize: 20, weight: .medium) // Larger icon - imageView.contentTintColor = .white // Ensure icon is visible on dark blur - stackView.addArrangedSubview(imageView) - } - - // Text - let textField = NSTextField(labelWithString: message) - textField.textColor = .white // Ensure text is visible - textField.font = .systemFont(ofSize: 14, weight: .medium) - textField.maximumNumberOfLines = 2 - textField.lineBreakMode = .byWordWrapping - stackView.addArrangedSubview(textField) - - // Constraints for StackView - NSLayoutConstraint.activate([ - stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15), // Padding - stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15), // Padding - stackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) - ]) - - window.contentView = contentView - return window - } - */ -} - -// MARK: - GridActionManagerDelegate Implementation -extension ScreenshotApp { - func getRenameActionHandler() -> RenameActionHandler { - return renameActionHandler - } - - func showOrUpdateStash(with imageURL: URL) { - // Call the existing implementation to avoid recursion - _showOrUpdateStash(with: imageURL, onScreen: nil) - } - - func closePreviewWithAnimation(immediate: Bool) { - previewManager.closePreviewWithAnimation(immediate: immediate, preserveTempFile: false) - } - - // NIEUW: Grid frame delegate method - func getGridWindowFrame() -> NSRect? { - return self.gridViewManager?.gridWindow?.frame - } -} - -// MARK: - UpdateManagerDelegate Implementation -extension ScreenshotApp: UpdateManagerDelegate { - func updateCheckDidStart() { - print("๐Ÿ”„ UPDATE: Update check started...") - } - - func updateCheckDidFinish() { - print("โœ… UPDATE: Update check finished") - } - - func updateAvailable(_ update: SUAppcastItem) { - print("๐ŸŽ‰ UPDATE: Update available - \(update.displayVersionString)") - print("๐Ÿ“‹ UPDATE: Current version: \(updateManager.currentVersion)") - print("๐Ÿ“‹ UPDATE: New version: \(update.displayVersionString)") - - // Show update notification - DispatchQueue.main.async { [weak self] in - let alert = NSAlert() - alert.messageText = "Update Available! ๐ŸŽ‰" - alert.informativeText = """ - ShotScreen \(update.displayVersionString) is now available. - You have version \(self?.updateManager.currentVersion ?? "unknown"). - - Would you like to download and install the update? - """ - alert.addButton(withTitle: "Download & Install") - alert.addButton(withTitle: "Skip This Version") - alert.addButton(withTitle: "Remind Me Later") - alert.alertStyle = .informational - - // Set app icon - if let appIcon = NSApplication.shared.applicationIconImage { - alert.icon = appIcon - } - - let response = alert.runModal() - switch response { - case .alertFirstButtonReturn: - print("โœ… UPDATE: User chose to download and install update") - // Sparkle will handle the download and installation automatically - case .alertSecondButtonReturn: - print("โŒ UPDATE: User chose to skip this version") - case .alertThirdButtonReturn: - print("โฐ UPDATE: User chose to be reminded later") - default: - break - } - } - } - - func noUpdateAvailable() { - print("โ„น๏ธ UPDATE: No updates available") - - // Show our simple, clean "no updates" popup - DispatchQueue.main.async { [weak self] in - print("โœ… UPDATE: Showing our custom 'no update' popup") - let alert = NSAlert() - alert.messageText = "You're up to date!" - alert.informativeText = "ShotScreen \(self?.updateManager.currentVersion ?? "unknown") is currently the newest version available." - alert.addButton(withTitle: "OK") - alert.alertStyle = .informational - alert.runModal() - } - } - - func updateCheckFailed(error: Error) { - print("โŒ UPDATE: Update check failed - \(error.localizedDescription)") - - DispatchQueue.main.async { - let alert = NSAlert() - alert.messageText = "Update Check Failed" - alert.informativeText = "Could not check for updates: \(error.localizedDescription)" - alert.addButton(withTitle: "OK") - alert.alertStyle = .warning - alert.runModal() - } - } -} - -// MARK: - App Entry Point -let app = NSApplication.shared -let delegate = ScreenshotApp() -app.delegate = delegate -app.run() - -extension NSScreen { - static func screenWithMouse(at point: NSPoint? = NSEvent.mouseLocation) -> NSScreen? { - guard let point = point else { return nil } - return NSScreen.screens.first { $0.frame.contains(point) } - } - - var displayID: CGDirectDisplayID { - return deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID ?? 0 - } - - // Add a helper to get localized name for logging, etc. - var customLocalizedName: String { // GEWIJZIGD - if #available(macOS 10.15, *) { - return self.localizedName // Dit roept de native property aan - } else { - // Fallback for older macOS versions if needed, or just use description - return "Screen \(displayID)" - } - } -} - - // ๐Ÿ”ง DEBUG: Force debug update check function for terminal testing - private func performForceDebugUpdate() { - print("๐Ÿ”ง DEBUG: Starting force debug update check...") - print("๐Ÿ”ง DEBUG: This will bypass development detection and show all Sparkle debug info") - print("๐Ÿ”ง DEBUG: ========================================") - - // Initialize UpdateManager and print debug info - UpdateManager.shared.printDebugInfo() - - // Call the force debug update check - UpdateManager.shared.forceDebugUpdateCheck() - - // Keep the run loop alive for a bit to see the results - DispatchQueue.main.asyncAfter(deadline: .now() + 10) { - print("๐Ÿ”ง DEBUG: Force debug update test completed") - print("๐Ÿ”ง DEBUG: Exiting in 2 seconds...") - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - NSApplication.shared.terminate(nil) - } - } - } diff --git a/ShotScreen/entitlements.plist b/ShotScreen/entitlements.plist deleted file mode 100644 index 8d7cadc..0000000 --- a/ShotScreen/entitlements.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.cs.screencapture - - - \ No newline at end of file diff --git a/bria-rmbg-coreml.mlpackage/Data/com.apple.CoreML/model.mlmodel b/bria-rmbg-coreml.mlpackage/Data/com.apple.CoreML/model.mlmodel deleted file mode 100644 index 1a70414..0000000 Binary files a/bria-rmbg-coreml.mlpackage/Data/com.apple.CoreML/model.mlmodel and /dev/null differ diff --git a/bria-rmbg-coreml.mlpackage/Data/com.apple.CoreML/weights/weight.bin b/bria-rmbg-coreml.mlpackage/Data/com.apple.CoreML/weights/weight.bin deleted file mode 100644 index 3aa5d59..0000000 Binary files a/bria-rmbg-coreml.mlpackage/Data/com.apple.CoreML/weights/weight.bin and /dev/null differ diff --git a/bria-rmbg-coreml.mlpackage/Manifest.json b/bria-rmbg-coreml.mlpackage/Manifest.json deleted file mode 100644 index 7e156cd..0000000 --- a/bria-rmbg-coreml.mlpackage/Manifest.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "fileFormatVersion": "1.0.0", - "itemInfoEntries": { - "216F8D6B-C0C1-4AE6-BEF9-618A1A41B9DD": { - "author": "com.apple.CoreML", - "description": "CoreML Model Weights", - "name": "weights", - "path": "com.apple.CoreML/weights" - }, - "B321596D-B0BF-46B8-8D28-69DBA4172E49": { - "author": "com.apple.CoreML", - "description": "CoreML Model Specification", - "name": "model.mlmodel", - "path": "com.apple.CoreML/model.mlmodel" - } - }, - "rootModelIdentifier": "B321596D-B0BF-46B8-8D28-69DBA4172E49" -} diff --git a/build_release_signed.sh b/build_release_signed.sh deleted file mode 100755 index 2a7a57f..0000000 --- a/build_release_signed.sh +++ /dev/null @@ -1,373 +0,0 @@ -#!/bin/bash - -# ShotScreen - Professional Build & Sign Script -# This script creates a properly signed and notarized build to prevent antivirus false positives - -set -e # Exit on any error - -echo "๐Ÿ”จ Starting professional build process for ShotScreen..." - -# Configuration -APP_NAME="ShotScreen" -BUNDLE_ID="com.shotscreen.app" -DEVELOPER_ID="Developer ID Application: Nick Roodenrijs (HX9B36NN8V)" -KEYCHAIN_PROFILE="ShotScreen-Profile" # Create this with 'xcrun notarytool store-credentials' - -# Build paths - FIXED: Use SwiftPM standard paths like build_app.sh -DIST_DIR="./dist" -APP_DIR="$DIST_DIR/$APP_NAME.app" -CONTENTS_DIR="$APP_DIR/Contents" -MACOS_DIR="$CONTENTS_DIR/MacOS" -RESOURCES_DIR="$CONTENTS_DIR/Resources" -FRAMEWORKS_DIR="$CONTENTS_DIR/Frameworks" - -# Create dist directory -rm -rf "$DIST_DIR" -mkdir -p "$DIST_DIR" - -# Read version from Info.plist for DMG naming -if [ -f "Info.plist" ]; then - APP_VERSION=$(plutil -extract CFBundleShortVersionString raw Info.plist 2>/dev/null || echo "1.0") -else - APP_VERSION="1.4" -fi - -DMG_PATH="./dist/$APP_NAME-$APP_VERSION.dmg" - -echo "๐Ÿ“ฆ Building release version..." -# FIXED: Use standard SwiftPM release build like build_app.sh does for debug -swift build --configuration release - -# FIXED: Get the path to the SwiftPM built binary (like build_app.sh) -BINARY_PATH=".build/release/ShotScreen" - -# Verify binary exists -if [ ! -f "$BINARY_PATH" ]; then - echo "โŒ Error: Built binary not found at $BINARY_PATH" - echo "Available files in .build/release/:" - ls -la .build/release/ || echo "No .build/release/ directory found" - exit 1 -fi - -echo "โœ… Found built binary at: $BINARY_PATH" - -echo "๐Ÿ—๏ธ Creating app bundle..." -# Create app bundle structure (like build_app.sh) -mkdir -p "$MACOS_DIR" -mkdir -p "$RESOURCES_DIR" -mkdir -p "$FRAMEWORKS_DIR" - -# Copy binary to app bundle and rename it to ShotScreen -echo "Copying binary to app bundle and renaming to ShotScreen..." -cp "$BINARY_PATH" "$MACOS_DIR/ShotScreen" - -# Verify the copy was successful -if [ -f "$MACOS_DIR/ShotScreen" ]; then - echo "โœ… Binary successfully copied to app bundle" - ls -la "$MACOS_DIR/ShotScreen" -else - echo "โŒ Error: Failed to copy binary to app bundle" - exit 1 -fi - -# Copy Info.plist -echo "Copying Info.plist..." -cp Info.plist "$CONTENTS_DIR/" - -# Copy app icon -echo "Copying app icon..." -if [ -f "./AppIcon.icns" ]; then - cp "./AppIcon.icns" "$RESOURCES_DIR/" - echo "โœ… Copied AppIcon.icns to app bundle Resources" -else - echo "โš ๏ธ Warning: AppIcon.icns not found" -fi - -# Copy resource images -if [ -d "ShotScreen/Sources/images" ]; then - cp -r "ShotScreen/Sources/images" "$RESOURCES_DIR/" 2>/dev/null || true - echo "โœ… Copied resource images" -fi - -# Copy frameworks (using same logic as build_app.sh) -echo "๐Ÿ“ฆ Copying frameworks..." - -# Find and copy Sparkle framework -SPARKLE_FRAMEWORK_PATH="" -for framework_path in .build/release/*.framework .build/*/release/*.framework .build/release/PackageFrameworks/*.framework; do - if [[ -d "$framework_path" && "$(basename "$framework_path")" == "Sparkle.framework" ]]; then - SPARKLE_FRAMEWORK_PATH="$framework_path" - break - fi -done - -if [[ -n "$SPARKLE_FRAMEWORK_PATH" && -d "$SPARKLE_FRAMEWORK_PATH" ]]; then - echo "โœ… Found Sparkle framework at: $SPARKLE_FRAMEWORK_PATH" - cp -R "$SPARKLE_FRAMEWORK_PATH" "$FRAMEWORKS_DIR/" - echo "โœ… Copied Sparkle framework to app bundle" -else - echo "โŒ Warning: Sparkle framework not found in SwiftPM build output" - echo "๐Ÿ” Searching in .build directory:" - find .build -name "*.framework" -type d 2>/dev/null || echo "No frameworks found" - - # Try alternative locations for Sparkle - if [ -d ".build/arm64-apple-macosx/release/Sparkle.framework" ]; then - cp -R ".build/arm64-apple-macosx/release/Sparkle.framework" "$FRAMEWORKS_DIR/" - echo "โœ… Copied Sparkle framework from arm64 location" - else - echo "โŒ Sparkle framework not found!" - exit 1 - fi -fi - -# Configure @rpath for framework loading (like build_app.sh) -echo "๐Ÿ”ง Configuring @rpath for framework loading..." -install_name_tool -add_rpath "@loader_path/../Frameworks" "$MACOS_DIR/ShotScreen" -if [ $? -eq 0 ]; then - echo "โœ… Added @rpath pointing to ../Frameworks directory" -else - echo "โŒ Failed to add @rpath configuration" -fi - -# Create PkgInfo file -echo "APPLSCR" > "$CONTENTS_DIR/PkgInfo" - -# Make the app bundle executable -chmod +x "$MACOS_DIR/ShotScreen" - -echo "โœ๏ธ Code signing with hardened runtime..." - -# Sign frameworks first -echo "๐Ÿ” Signing frameworks..." -for framework in "$FRAMEWORKS_DIR"/*.framework; do - if [ -d "$framework" ]; then - echo " Signing framework: $(basename "$framework")" - codesign --sign "$DEVELOPER_ID" \ - --force \ - --timestamp \ - --options runtime \ - "$framework" - echo " โœ… Signed framework: $(basename "$framework")" - fi -done - -# Sign with hardened runtime to prevent malware-like behavior detection -echo "๐Ÿ” Signing main app bundle..." -codesign --sign "$DEVELOPER_ID" \ - --deep \ - --force \ - --options runtime \ - --entitlements ShotScreen/entitlements.plist \ - --timestamp \ - "$APP_DIR" - -echo "๐Ÿ” Verifying signature..." -codesign --verify --deep --strict "$APP_DIR" -echo "โœ… Code signature is valid" - -# Note: spctl assessment may fail for unnotarized apps, but that's okay for local testing -if spctl --assess --type exec "$APP_DIR" 2>/dev/null; then - echo "โœ… Gatekeeper assessment passed" -else - echo "โš ๏ธ Gatekeeper assessment failed (app needs notarization for distribution)" -fi - -echo "๐Ÿ“ฆ Creating professional DMG with custom background and layout..." -# FIXED: Use unique volume name to avoid conflicts with existing ShotScreen volume -DMG_VOLUME_NAME="$APP_NAME-$APP_VERSION" -# First unmount any existing volumes to prevent conflicts -hdiutil detach "/Volumes/$DMG_VOLUME_NAME" 2>/dev/null || true - -# Create a temporary folder for DMG contents -TEMP_DMG_DIR="./temp_dmg" -rm -rf "$TEMP_DMG_DIR" -mkdir -p "$TEMP_DMG_DIR" - -# Copy app to temp folder -cp -R "$APP_DIR" "$TEMP_DMG_DIR/" - -# Create symlink to Applications folder -echo "๐Ÿ”— Creating Applications symlink..." -ln -s /Applications "$TEMP_DMG_DIR/Applications" - -# Copy background image to temp folder -echo "๐ŸŽจ Adding custom background image..." -if [ -f "./ShotScreen/Sources/images/BannerFinder.png" ]; then - mkdir -p "$TEMP_DMG_DIR/.background" - cp "./ShotScreen/Sources/images/BannerFinder.png" "$TEMP_DMG_DIR/.background/background.png" - echo "โœ… Background image added (BannerFinder.png)" -else - echo "โš ๏ธ Warning: BannerFinder.png not found in ShotScreen/Sources/images/" -fi - -# Create initial DMG (read-write for customization) -TEMP_DMG_PATH="./temp_dmg_rw.dmg" -rm -f "$TEMP_DMG_PATH" - -hdiutil create -volname "$DMG_VOLUME_NAME" \ - -srcfolder "$TEMP_DMG_DIR" \ - -ov \ - -format UDRW \ - -size 100m \ - "$TEMP_DMG_PATH" - -if [ $? -ne 0 ]; then - echo "โŒ DMG creation failed!" - echo "๐Ÿ” Checking if volume is still mounted..." - mount | grep -i shotscreen || echo "No ShotScreen volumes mounted" - # Clean up temp folder - rm -rf "$TEMP_DMG_DIR" - exit 1 -fi - -# Mount the DMG for customization -echo "๐Ÿ”ง Mounting DMG for customization..." -hdiutil attach "$TEMP_DMG_PATH" -mountpoint "/Volumes/$DMG_VOLUME_NAME" -nobrowse - -# Wait for mount to complete -sleep 2 - -# Customize DMG appearance with AppleScript -echo "โœจ Customizing DMG appearance with background and layout..." -cat > dmg_style.applescript << 'EOF' -tell application "Finder" - tell disk "DMG_VOLUME_NAME_PLACEHOLDER" - open - - -- Wait for window to open - delay 3 - - set current view of container window to icon view - set toolbar visible of container window to false - set statusbar visible of container window to false - - -- Smaller window size (500x350 instead of 500x300) - set the bounds of container window to {300, 100, 800, 450} - - set theViewOptions to the icon view options of container window - set arrangement of theViewOptions to not arranged - set icon size of theViewOptions to 96 -- Large icons - set text size of theViewOptions to 12 - - -- Set background picture - set background picture of theViewOptions to file ".background:background.png" - - -- Wait for settings to apply - delay 2 - - -- Position icons with more space - set position of item "ShotScreen.app" of container window to {150, 200} - set position of item "Applications" of container window to {350, 200} - - -- Force update and wait - update without registering applications - delay 3 - - -- Close and reopen to ensure settings stick - close - delay 1 - open - - delay 2 - end tell -end tell -EOF - -# Replace placeholder with actual volume name -sed -i '' "s/DMG_VOLUME_NAME_PLACEHOLDER/$DMG_VOLUME_NAME/g" dmg_style.applescript - -# Apply the styling -echo "๐ŸŽจ Applying AppleScript styling..." -if osascript dmg_style.applescript; then - echo "โœ… AppleScript styling applied successfully" -else - echo "โš ๏ธ AppleScript styling failed, trying alternative method..." - - # Alternative: Set view manually via Finder automation - cat > dmg_style_alt.applescript << 'EOF' -tell application "Finder" - tell disk "DMG_VOLUME_NAME_PLACEHOLDER" - open - delay 5 - - -- Simple settings that usually work - set current view of container window to icon view - delay 1 - - set theViewOptions to the icon view options of container window - set icon size of theViewOptions to 96 - delay 1 - - close - delay 1 - open - delay 2 - end tell -end tell -EOF - - # Replace placeholder and run alternative - sed -i '' "s/DMG_VOLUME_NAME_PLACEHOLDER/$DMG_VOLUME_NAME/g" dmg_style_alt.applescript - osascript dmg_style_alt.applescript 2>/dev/null || echo "โš ๏ธ Alternative styling also failed" - rm -f dmg_style_alt.applescript -fi - -# Clean up AppleScript -rm -f dmg_style.applescript - -# Force create .DS_Store with proper settings -echo "๐Ÿ“ Setting Finder view options..." -if [ -d "/Volumes/$DMG_VOLUME_NAME" ]; then - # Ensure background image is accessible - if [ -f "/Volumes/$DMG_VOLUME_NAME/.background/background.png" ]; then - echo "โœ… Background image found in DMG" - else - echo "โš ๏ธ Background image not found in DMG" - fi - - # Force Finder to acknowledge the DMG - osascript -e "tell application \"Finder\" to open disk \"$DMG_VOLUME_NAME\"" 2>/dev/null || true - sleep 3 - - # Let Finder settle and write .DS_Store - sleep 5 -fi - -# Unmount the DMG -echo "๐Ÿ“ค Unmounting DMG..." -hdiutil detach "/Volumes/$DMG_VOLUME_NAME" - -# Convert to compressed read-only DMG -echo "๐Ÿ—œ๏ธ Converting to final compressed DMG..." -hdiutil convert "$TEMP_DMG_PATH" \ - -format UDZO \ - -o "$DMG_PATH" - -# Clean up temporary files -rm -f "$TEMP_DMG_PATH" -rm -rf "$TEMP_DMG_DIR" - -echo "โœ… Professional DMG created successfully with custom background and layout!" - -echo "โœ๏ธ Signing DMG..." -codesign --sign "$DEVELOPER_ID" \ - --timestamp \ - "$DMG_PATH" - -echo "๐Ÿ“ค Submitting for notarization..." -if xcrun notarytool submit "$DMG_PATH" --keychain-profile "$KEYCHAIN_PROFILE" --wait; then - echo "โœ… Notarization successful!" - echo "๐Ÿ” Stapling notarization ticket to DMG..." - xcrun stapler staple "$DMG_PATH" - echo "โœ… DMG is now notarized and ready for distribution!" -else - echo "โš ๏ธ Notarization failed or timed out" - echo "๐Ÿ’ก You can check status with: xcrun notarytool history --keychain-profile $KEYCHAIN_PROFILE" - echo "๐Ÿ’ก Manual stapling: xcrun stapler staple $DMG_PATH" -fi - -echo "" -echo "โœ… Build complete!" -echo "๐Ÿ“ฆ DMG created at: $DMG_PATH" -echo "โœ… Code signed with Developer ID: Nick Roodenrijs (HX9B36NN8V)" -echo "๐ŸŽฏ DMG ready for hufterproof release script!" \ No newline at end of file diff --git a/dist/ShotScreen-1.0.dmg b/dist/ShotScreen-1.0.dmg deleted file mode 100644 index d69c324..0000000 Binary files a/dist/ShotScreen-1.0.dmg and /dev/null differ diff --git a/release_hufterproof_v2.sh b/release_hufterproof_v2.sh deleted file mode 100755 index 7c359e3..0000000 --- a/release_hufterproof_v2.sh +++ /dev/null @@ -1,320 +0,0 @@ -#!/bin/bash - -# ๐Ÿš€ SUPER HUFTERPROOF ShotScreen Release Script V2 -# Zero HTML, Zero Problems, 100% Bulletproof -# ๐ŸŽฏ NEW: Auto-increment version detection! -# -# Usage: -# ./release_hufterproof_v2.sh # Auto-increment from current version -# ./release_hufterproof_v2.sh 1.15 # Use specific version - -set -e - -# Colors -GREEN='\033[0;32m' -BLUE='\033[0;34m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -PURPLE='\033[0;35m' -NC='\033[0m' - -# Config -GITEA_URL="https://git.plet.i234.me" -RELEASES_REPO="Nick/shotscreen" - -echo -e "${PURPLE}๐Ÿš€ SUPER HUFTERPROOF Release Script V2${NC}" -echo -e "${PURPLE}=====================================${NC}" - -# Get current version from Info.plist and auto-increment -get_next_version() { - if [ -f "Info.plist" ]; then - CURRENT_VERSION=$(plutil -extract CFBundleShortVersionString raw Info.plist 2>/dev/null || echo "1.0") - echo -e "${BLUE}๐Ÿ” Current version: $CURRENT_VERSION${NC}" - - # Split version into parts (e.g., "1.12" -> major=1, minor=12) - IFS='.' read -r MAJOR MINOR <<< "$CURRENT_VERSION" - - # Increment minor version - NEXT_MINOR=$((MINOR + 1)) - SUGGESTED_VERSION="$MAJOR.$NEXT_MINOR" - - echo -e "${GREEN}๐Ÿš€ Suggested next version: $SUGGESTED_VERSION${NC}" - return 0 - else - SUGGESTED_VERSION="1.0" - echo -e "${YELLOW}โš ๏ธ No Info.plist found, suggesting: $SUGGESTED_VERSION${NC}" - return 1 - fi -} - -# Determine version -NEW_VERSION="$1" -if [ -z "$NEW_VERSION" ]; then - get_next_version - echo -e "${YELLOW}Press ENTER to use suggested version ($SUGGESTED_VERSION) or type custom version:${NC}" - read -r USER_INPUT - - if [ -z "$USER_INPUT" ]; then - NEW_VERSION="$SUGGESTED_VERSION" - echo -e "${GREEN}โœ… Using suggested version: $NEW_VERSION${NC}" - else - NEW_VERSION="$USER_INPUT" - echo -e "${BLUE}โœ… Using custom version: $NEW_VERSION${NC}" - fi -fi - -echo -e "${BLUE}๐Ÿ“‹ Building version: $NEW_VERSION${NC}" - -# Update Info.plist -echo -e "${BLUE}๐Ÿ“ Updating Info.plist...${NC}" -plutil -replace CFBundleVersion -string "$NEW_VERSION" Info.plist -plutil -replace CFBundleShortVersionString -string "$NEW_VERSION" Info.plist - -# Update build_release_signed.sh with new version -echo -e "${BLUE}๐Ÿ“ Updating build script version...${NC}" -sed -i '' "s/APP_VERSION=\".*\"/APP_VERSION=\"$NEW_VERSION\"/" build_release_signed.sh - -# Build app with Developer ID signing and notarization -echo -e "${BLUE}๐Ÿ”จ Building app with Developer ID signing...${NC}" -if ! ./build_release_signed.sh; then - echo -e "${RED}โŒ Build failed${NC}" - exit 1 -fi - -# Check DMG exists -DMG_PATH="./dist/ShotScreen-$NEW_VERSION.dmg" -if [ ! -f "$DMG_PATH" ]; then - echo -e "${RED}โŒ DMG not found: $DMG_PATH${NC}" - exit 1 -fi - -# Get signature and size -echo -e "${BLUE}๐Ÿ” Getting signature...${NC}" -SIG_OUTPUT=$(./.build/artifacts/sparkle/Sparkle/bin/sign_update "$DMG_PATH") -SIGNATURE=$(echo "$SIG_OUTPUT" | grep -o 'sparkle:edSignature="[^"]*"' | cut -d'"' -f2) -DMG_SIZE=$(stat -f%z "$DMG_PATH") - -if [ -z "$SIGNATURE" ]; then - echo -e "${RED}โŒ Signature generation failed${NC}" - exit 1 -fi - -echo -e "${GREEN}โœ… Signature: ${SIGNATURE:0:20}...${NC}" -echo -e "${GREEN}โœ… Size: $DMG_SIZE bytes${NC}" - -# Get release notes from TXT file (preserve original formatting) -RELEASE_NOTES=$(awk ' - /^ShotScreen [0-9]/ { - if (count++ > 0) exit - next - } - /^=+$/ { next } - /^[A-Za-z]/ && !/^ShotScreen/ && NF > 0 { - print $0 - } - /^ / && NF > 0 { - print $0 - } -' release_notes.txt) - -if [ -z "$RELEASE_NOTES" ]; then - RELEASE_NOTES="Release version $NEW_VERSION" -fi - -# Create Gitea release -echo -e "${BLUE}๐Ÿ“ฆ Creating Gitea release...${NC}" - -# Format release notes beautifully for Gitea (markdown format) -FORMATTED_NOTES="## ๐Ÿš€ ShotScreen $NEW_VERSION Features - -$RELEASE_NOTES" - -# Clean JSON (proper escaping for multiline text) -CLEAN_NOTES=$(printf "%s" "$FORMATTED_NOTES" | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}' | sed 's/\\n$//') - -JSON_PAYLOAD="{ - \"tag_name\": \"v$NEW_VERSION\", - \"name\": \"ShotScreen v$NEW_VERSION\", - \"body\": \"$CLEAN_NOTES\", - \"draft\": false, - \"prerelease\": false -}" - -RESPONSE=$(curl -s -X POST \ - -H "Authorization: token $GITEA_TOKEN" \ - -H "Content-Type: application/json" \ - -d "$JSON_PAYLOAD" \ - "$GITEA_URL/api/v1/repos/$RELEASES_REPO/releases") - -RELEASE_ID=$(echo "$RESPONSE" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2) - -if [ -z "$RELEASE_ID" ]; then - echo -e "${YELLOW}โš ๏ธ Checking for existing release...${NC}" - EXISTING=$(curl -s "$GITEA_URL/api/v1/repos/$RELEASES_REPO/releases/tags/v$NEW_VERSION") - RELEASE_ID=$(echo "$EXISTING" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2) -fi - -if [ -z "$RELEASE_ID" ]; then - echo -e "${RED}โŒ Could not create/find release${NC}" - exit 1 -fi - -echo -e "${GREEN}โœ… Release ID: $RELEASE_ID${NC}" - -# Upload DMG -echo -e "${BLUE}๐Ÿ“ค Uploading DMG...${NC}" -UPLOAD_RESPONSE=$(curl -s -X POST \ - -H "Authorization: token $GITEA_TOKEN" \ - -F "attachment=@$DMG_PATH" \ - "$GITEA_URL/api/v1/repos/$RELEASES_REPO/releases/$RELEASE_ID/assets") - -ASSET_ID=$(echo "$UPLOAD_RESPONSE" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2) - -if [ -z "$ASSET_ID" ]; then - echo -e "${RED}โŒ DMG upload failed${NC}" - exit 1 -fi - -echo -e "${GREEN}โœ… DMG uploaded${NC}" - -# Create super simple appcast (no HTML, just plain text) -echo -e "${BLUE}๐Ÿ“„ Creating simple appcast...${NC}" - -# Convert plain text to simple HTML for appcast -HTML_NOTES=$(echo "$RELEASE_NOTES" | sed 's/^- /
  • /' | sed 's/$/<\/li>/') -if [[ "$HTML_NOTES" == *"
  • "* ]]; then - HTML_NOTES="
      $HTML_NOTES
    " -fi - -# Clean up any old appcast first -rm -f appcast.xml - -echo -e "${BLUE}๐Ÿ“„ Generating appcast for version $NEW_VERSION...${NC}" -cat > appcast.xml << EOF - - - - ShotScreen Updates - $GITEA_URL/$RELEASES_REPO/raw/branch/main/appcast.xml - ShotScreen Updates - en - - - ShotScreen $NEW_VERSION - ShotScreen $NEW_VERSION - $HTML_NOTES - ]]> - $(date -R) - - 13.0 - - - -EOF - -# Validate appcast -if ! xmllint --noout appcast.xml 2>/dev/null; then - echo -e "${RED}โŒ Appcast validation failed${NC}" - exit 1 -fi - -echo -e "${GREEN}โœ… Valid appcast created${NC}" - -# Debug: Show what we're about to deploy -echo -e "${BLUE}๐Ÿ” DEBUG: Appcast content preview:${NC}" -echo "Version: $(grep 'sparkle:version=' appcast.xml | head -1)" -echo "URL: $(grep 'enclosure url=' appcast.xml | head -1 | cut -d'"' -f2)" - -# Deploy appcast -echo -e "${BLUE}๐Ÿš€ Deploying appcast...${NC}" - -RELEASES_DIR="../shotscreen" - -# Clean up any existing problematic repository -if [ -d "$RELEASES_DIR" ]; then - echo -e "${YELLOW}๐Ÿงน Cleaning up existing repository...${NC}" - rm -rf "$RELEASES_DIR" -fi - -# Create fresh directory and clone -mkdir -p "$RELEASES_DIR" -cd "$RELEASES_DIR" - -echo -e "${BLUE}๐Ÿ“ฅ Fresh clone from remote...${NC}" -git clone "$GITEA_URL/$RELEASES_REPO.git" . || { - echo -e "${YELLOW}โš ๏ธ Remote repository doesn't exist or clone failed, creating new repo...${NC}" - git init - git remote add origin "$GITEA_URL/$RELEASES_REPO.git" -} - -# Configure git for this repository -git config pull.rebase false # Use merge instead of rebase -git config user.name "ShotScreen Release Bot" || true -git config user.email "releases@shotscreen.app" || true - -# Backup old appcast for comparison -if [ -f "appcast.xml" ]; then - echo -e "${BLUE}๐Ÿ“‹ Backing up old appcast...${NC}" - cp "appcast.xml" "appcast.xml.backup" - echo "Old version: $(grep 'sparkle:version=' "appcast.xml" | head -1)" -fi - -# Copy new appcast -cd - > /dev/null -cp appcast.xml "$RELEASES_DIR/" -cd "$RELEASES_DIR" - -# Add and commit -git add appcast.xml -git commit -m "Deploy appcast for v$NEW_VERSION" || true - -# Push with force if needed (since we're authoritative for appcast) -echo -e "${BLUE}๐Ÿ“ค Pushing appcast to remote...${NC}" -if ! git push origin main; then - echo -e "${YELLOW}โš ๏ธ Normal push failed, force pushing appcast update...${NC}" - git push origin main --force -fi - -cd - > /dev/null - -# Test everything -echo -e "${BLUE}๐Ÿงช Testing...${NC}" -sleep 2 - -# Test appcast download -if ! curl -s "$GITEA_URL/$RELEASES_REPO/raw/branch/main/appcast.xml" | grep -q "$NEW_VERSION"; then - echo -e "${RED}โŒ Appcast test failed${NC}" - exit 1 -fi - -# Test DMG download -if ! curl -I -s "$GITEA_URL/$RELEASES_REPO/releases/download/v$NEW_VERSION/ShotScreen-$NEW_VERSION.dmg" | grep -q "200"; then - echo -e "${RED}โŒ DMG test failed${NC}" - exit 1 -fi - -# Git operations -echo -e "${BLUE}๐Ÿ“ Git operations...${NC}" -git add . -git commit -m "Release v$NEW_VERSION" || true -git tag -a "v$NEW_VERSION" -m "Release v$NEW_VERSION" || true -git push -u origin main || git push origin main --force -git push origin "v$NEW_VERSION" || true - -echo -echo -e "${GREEN}๐ŸŽ‰ SUPER HUFTERPROOF RELEASE COMPLETE! ๐ŸŽ‰${NC}" -echo -e "${GREEN}======================================${NC}" -echo -e "${GREEN}โœ… Version: $NEW_VERSION${NC}" -echo -e "${GREEN}โœ… DMG Size: $DMG_SIZE bytes${NC}" -echo -e "${GREEN}โœ… All tests: PASSED${NC}" -echo -echo -e "${BLUE}๐ŸŒ Release: $GITEA_URL/$RELEASES_REPO/releases/tag/v$NEW_VERSION${NC}" -echo -e "${BLUE}๐Ÿ“ก Appcast: $GITEA_URL/$RELEASES_REPO/raw/branch/main/appcast.xml${NC}" -echo -echo -e "${YELLOW}๐ŸŽฏ Ready to test updates on other computer!${NC}" \ No newline at end of file