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",
- "