🎉 ShotScreen v1.0 - Initial Release

🚀 First official release of ShotScreen with complete feature set:

 Core Features:
- Advanced screenshot capture system
- Multi-monitor support
- Professional UI/UX design
- Automated update system with Sparkle
- Apple notarized & code signed

🛠 Technical Excellence:
- Native Swift macOS application
- Professional build & deployment pipeline
- Comprehensive error handling
- Memory optimized performance

📦 Distribution Ready:
- Professional DMG packaging
- Apple notarization complete
- No security warnings for users
- Ready for public distribution

This is the foundation release that establishes ShotScreen as a premium screenshot tool for macOS.
This commit is contained in:
2025-06-28 16:15:15 +02:00
commit 0dabed11d2
63 changed files with 25727 additions and 0 deletions
BIN
View File
Binary file not shown.
+67
View File
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>ShotScreen</string>
<key>CFBundleExecutable</key>
<string>ShotScreen</string>
<key>CFBundleGetInfoString</key>
<string>ShotScreen - Professional Screenshot Tool</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>CFBundleIdentifier</key>
<string>com.shotscreen.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>ShotScreen</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>
<string>13.0</string>
<key>LSUIElement</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.3</string>
<key>NSRequiresCertificateTransparency</key>
<true/>
</dict>
<key>NSApplicationDescription</key>
<string>Professional screenshot and screen capture utility for macOS</string>
<key>NSCameraUsageDescription</key>
<string>ShotScreen needs camera access to capture screenshots.</string>
<key>NSDesktopFolderUsageDescription</key>
<string>ShotScreen needs access to save screenshots to your Desktop.</string>
<key>NSDocumentsFolderUsageDescription</key>
<string>ShotScreen needs access to save screenshots to your Documents folder.</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>© 2025 ShotScreen. All rights reserved.</string>
<key>NSScreenCaptureUsageDescription</key>
<string>ShotScreen is a professional screenshot tool that captures images of your screen and windows to help you save and organize screenshots efficiently.</string>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
<key>SUAutomaticallyUpdate</key>
<true/>
<key>SUCheckAtStartup</key>
<true/>
<key>SUFeedURL</key>
<string>https://git.plet.i234.me/Nick/shotscreen/raw/branch/main/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>q0Ia/obtuDugqhwa1aSZsQAqZxQsdgX3y4K9wuqkemM=</string>
<key>SUScheduledCheckInterval</key>
<integer>86400</integer>
</dict>
</plist>
+23
View File
@@ -0,0 +1,23 @@
{
"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
}
Executable
+59
View File
@@ -0,0 +1,59 @@
// 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")
]
)
]
)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
XSym
0024
65d970057a31dc065e5b25921548a548
arm64-apple-macosx/debug
+100
View File
@@ -0,0 +1,100 @@
client:
name: basic
file-system: device-agnostic
tools: {}
targets:
"HotKey-arm64-apple-macosx15.0-debug.module": ["<HotKey-arm64-apple-macosx15.0-debug.module>"]
"PackageStructure": ["<PackageStructure>"]
"ScreenShot-arm64-apple-macosx15.0-debug.exe": ["<ScreenShot-arm64-apple-macosx15.0-debug.exe>"]
"ScreenShot-arm64-apple-macosx15.0-debug.module": ["<ScreenShot-arm64-apple-macosx15.0-debug.module>"]
"main": ["<ScreenShot-arm64-apple-macosx15.0-debug.exe>","<ScreenShot-arm64-apple-macosx15.0-debug.module>"]
"test": ["<ScreenShot-arm64-apple-macosx15.0-debug.exe>","<ScreenShot-arm64-apple-macosx15.0-debug.module>"]
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: ["<sources-file-list>","/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: ["<entitlement-plist>","<com.apple.security.get-task-allow>"]
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: ["<sources-file-list>","/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: ["<link-file-list>","/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: ["<swift-get-version>","/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"
"<HotKey-arm64-apple-macosx15.0-debug.module>":
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: ["<HotKey-arm64-apple-macosx15.0-debug.module>"]
"<ScreenShot-arm64-apple-macosx15.0-debug.exe>":
tool: phony
inputs: ["<ScreenShot-arm64-apple-macosx15.0-debug.exe-CodeSigning>"]
outputs: ["<ScreenShot-arm64-apple-macosx15.0-debug.exe>"]
"<ScreenShot-arm64-apple-macosx15.0-debug.module>":
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: ["<ScreenShot-arm64-apple-macosx15.0-debug.module>"]
"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: ["<ScreenShot-arm64-apple-macosx15.0-debug.exe-CodeSigning>"]
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: ["<PackageStructure>"]
description: "Planning build"
allow-missing-inputs: true
+100
View File
@@ -0,0 +1,100 @@
client:
name: basic
file-system: device-agnostic
tools: {}
targets:
"HotKey-arm64-apple-macosx15.0-debug.module": ["<HotKey-arm64-apple-macosx15.0-debug.module>"]
"PackageStructure": ["<PackageStructure>"]
"ScreenShot-arm64-apple-macosx15.0-debug.exe": ["<ScreenShot-arm64-apple-macosx15.0-debug.exe>"]
"ScreenShot-arm64-apple-macosx15.0-debug.module": ["<ScreenShot-arm64-apple-macosx15.0-debug.module>"]
"main": ["<ScreenShot-arm64-apple-macosx15.0-debug.exe>","<ScreenShot-arm64-apple-macosx15.0-debug.module>"]
"test": ["<ScreenShot-arm64-apple-macosx15.0-debug.exe>","<ScreenShot-arm64-apple-macosx15.0-debug.module>"]
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: ["<sources-file-list>","/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: ["<entitlement-plist>","<com.apple.security.get-task-allow>"]
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: ["<sources-file-list>","/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: ["<link-file-list>","/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: ["<swift-get-version>","/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"
"<HotKey-arm64-apple-macosx15.0-debug.module>":
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: ["<HotKey-arm64-apple-macosx15.0-debug.module>"]
"<ScreenShot-arm64-apple-macosx15.0-debug.exe>":
tool: phony
inputs: ["<ScreenShot-arm64-apple-macosx15.0-debug.exe-CodeSigning>"]
outputs: ["<ScreenShot-arm64-apple-macosx15.0-debug.exe>"]
"<ScreenShot-arm64-apple-macosx15.0-debug.module>":
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: ["<ScreenShot-arm64-apple-macosx15.0-debug.module>"]
"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: ["<ScreenShot-arm64-apple-macosx15.0-debug.exe-CodeSigning>"]
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: ["<PackageStructure>"]
description: "Planning build"
allow-missing-inputs: true
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,236 @@
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
}
}
+88
View File
@@ -0,0 +1,88 @@
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)"
}
}
+379
View File
@@ -0,0 +1,379 @@
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()
}
}
}
+186
View File
@@ -0,0 +1,186 @@
//
// 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
}
}
+213
View File
@@ -0,0 +1,213 @@
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: [:])
}
}
+280
View File
@@ -0,0 +1,280 @@
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)
}
}
}
@@ -0,0 +1,163 @@
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()
}
}
@@ -0,0 +1,64 @@
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()
}
}
+606
View File
@@ -0,0 +1,606 @@
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<AnyView>?
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
+443
View File
@@ -0,0 +1,443 @@
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")
}
+194
View File
@@ -0,0 +1,194 @@
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
}
}
+515
View File
@@ -0,0 +1,515 @@
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)
}
}
}
File diff suppressed because it is too large Load Diff
+473
View File
@@ -0,0 +1,473 @@
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()
}
+605
View File
@@ -0,0 +1,605 @@
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")
}
+783
View File
@@ -0,0 +1,783 @@
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 }
}
File diff suppressed because it is too large Load Diff
+253
View File
@@ -0,0 +1,253 @@
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)
}
}
File diff suppressed because it is too large Load Diff
+711
View File
@@ -0,0 +1,711 @@
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
}
}
@@ -0,0 +1,318 @@
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<NSImage?, Never>?
// 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
}
}
}
+962
View File
@@ -0,0 +1,962 @@
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
}
}
+413
View File
@@ -0,0 +1,413 @@
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)"
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+83
View File
@@ -0,0 +1,83 @@
import SwiftUI
// MARK: - Multi-Monitor Selection Overlay View
struct MultiMonitorSelectionOverlayView: View {
@State private var animationOffset: CGFloat = 0
var body: some View {
ZStack {
// Semi-transparent background with subtle gradient
Rectangle()
.fill(
LinearGradient(
gradient: Gradient(colors: [
Color.blue.opacity(0.15),
Color.blue.opacity(0.25)
]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
// Animated border
Rectangle()
.stroke(
LinearGradient(
gradient: Gradient(colors: [
Color.blue.opacity(0.8),
Color.cyan.opacity(0.6),
Color.blue.opacity(0.8)
]),
startPoint: .leading,
endPoint: .trailing
),
lineWidth: 2
)
.overlay(
// Animated dashed border for better visibility
Rectangle()
.stroke(
Color.white.opacity(0.8),
style: StrokeStyle(
lineWidth: 1,
dash: [8, 4],
dashPhase: animationOffset
)
)
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onAppear {
// Start the dash animation
withAnimation(.linear(duration: 2).repeatForever(autoreverses: false)) {
animationOffset = 24
}
}
}
}
// MARK: - Crosshair Overlay View
struct CrosshairOverlayView: View {
let mouseLocation: CGPoint
private let crosshairSize: CGFloat = 20
var body: some View {
ZStack {
// Horizontal line
Rectangle()
.fill(Color.white)
.frame(width: crosshairSize, height: 2)
.shadow(color: .black, radius: 1, x: 0, y: 0)
// Vertical line
Rectangle()
.fill(Color.white)
.frame(width: 2, height: crosshairSize)
.shadow(color: .black, radius: 1, x: 0, y: 0)
}
.position(mouseLocation)
.allowsHitTesting(false) // Don't interfere with mouse events
}
}
+176
View File
@@ -0,0 +1,176 @@
import AppKit
import SwiftUI
// MARK: - Centralized Theme Manager
class ThemeManager {
static let shared = ThemeManager()
private init() {}
// MARK: - System-Aware Colors (automatic light/dark adaptation)
/// Background colors for containers (GLASS EFFECT zoals stash)
var containerBackground: NSColor {
// NIEUW: Glass effect achtergrond zoals stash window
// Gebruik transparante achtergrond zodat visual effect view erdoor kan
return NSColor.clear
}
/// Glass effect background for visual effect views
var glassEffectMaterial: NSVisualEffectView.Material {
return .hudWindow // Exact hetzelfde als stash window
}
/// Glass effect blending mode
var glassEffectBlending: NSVisualEffectView.BlendingMode {
return .behindWindow // Exact hetzelfde als stash window
}
/// Glass effect alpha
var glassEffectAlpha: CGFloat {
return 0.95 // Exact hetzelfde als stash window
}
/// Background colors for secondary containers
var secondaryContainerBackground: NSColor {
if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua {
return NSColor(white: 0.42, alpha: 0.90) // Dark mode: iets lichter
} else {
return NSColor(white: 0.98, alpha: 0.90) // Light mode: bijna wit
}
}
/// Button and icon colors (adaptive)
var buttonTintColor: NSColor {
if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua {
return NSColor(white: 0.95, alpha: 1.0) // Dark mode: wit
} else {
return NSColor(white: 0.25, alpha: 1.0) // Light mode: donkergrijs
}
}
/// Text colors (adaptive)
var primaryTextColor: NSColor {
if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua {
return NSColor.white // Dark mode: wit
} else {
return NSColor.black // Light mode: zwart
}
}
var secondaryTextColor: NSColor {
if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua {
return NSColor(white: 0.7, alpha: 1.0) // Dark mode: grijs
} else {
return NSColor(white: 0.4, alpha: 1.0) // Light mode: donkergrijs
}
}
/// Filename label text color (adaptive with opacity)
var filenameLabelTextColor: NSColor {
if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua {
return NSColor(white: 0.7, alpha: 1.0) // Dark mode: lichtgrijs
} else {
return NSColor(white: 0.1, alpha: 1.0) // Light mode: veel donkerder voor betere zichtbaarheid
}
}
/// Grid cell icon background (adaptive)
var gridCellIconBackground: NSColor {
if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua {
return NSColor.white.withAlphaComponent(0.25) // Dark mode
} else {
return NSColor.black.withAlphaComponent(0.25) // Light mode
}
}
/// Grid cell text color with hover effect
func gridCellTextColor(isHovered: Bool) -> NSColor {
if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua {
return isHovered ? NSColor.white : NSColor.white.withAlphaComponent(0.1)
} else {
return isHovered ? NSColor.black : NSColor.black.withAlphaComponent(0.1)
}
}
// MARK: - Shadow Configuration (adaptive)
var shadowColor: NSColor {
if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua {
return NSColor.black // Dark mode: black shadows
} else {
return NSColor.black.withAlphaComponent(0.3) // Light mode: lighter shadows
}
}
var shadowOpacity: Float {
if NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua {
return 0.50 // Dark mode: stronger shadows
} else {
return 0.25 // Light mode: subtle shadows
}
}
// MARK: - Utility Methods
/// Check if currently in dark mode
var isDarkMode: Bool {
return NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua
}
/// Debug function to print current theme info
func printCurrentTheme() {
let mode = isDarkMode ? "DARK" : "LIGHT"
print("🎨 THEME DEBUG: Current mode = \(mode)")
print("🎨 THEME DEBUG: Container background = GLASS EFFECT (\(glassEffectMaterial.rawValue))")
print("🎨 THEME DEBUG: Glass effect alpha = \(glassEffectAlpha)")
print("🎨 THEME DEBUG: Button tint = \(buttonTintColor)")
print("🎨 THEME DEBUG: Primary text = \(primaryTextColor)")
print("🎨 THEME DEBUG: Shadow opacity = \(shadowOpacity)")
}
/// Get hover color for buttons
var buttonHoverColor: NSColor {
if isDarkMode {
return NSColor.white
} else {
return NSColor.black
}
}
/// Get original button color (for hover restoration)
var buttonOriginalColor: NSColor {
return buttonTintColor
}
// MARK: - Notification for Theme Changes
func observeThemeChanges(callback: @escaping () -> Void) {
// Observer voor system appearance changes
DistributedNotificationCenter.default.addObserver(
forName: NSNotification.Name("AppleInterfaceThemeChangedNotification"),
object: nil,
queue: .main
) { _ in
print("🎨 THEME: System theme changed - notifying observers")
callback()
}
}
}
// MARK: - SwiftUI Color Extensions
extension Color {
static var adaptiveContainerBackground: Color {
Color(ThemeManager.shared.containerBackground)
}
static var adaptiveSecondaryBackground: Color {
Color(ThemeManager.shared.secondaryContainerBackground)
}
static var adaptivePrimaryText: Color {
Color(ThemeManager.shared.primaryTextColor)
}
static var adaptiveSecondaryText: Color {
Color(ThemeManager.shared.secondaryTextColor)
}
}
+446
View File
@@ -0,0 +1,446 @@
import Foundation
import AppKit
import Sparkle
// MARK: - Update Manager Delegate Protocol
protocol UpdateManagerDelegate: AnyObject {
func updateCheckDidStart()
func updateCheckDidFinish()
func updateAvailable(_ update: SUAppcastItem)
func noUpdateAvailable()
func updateCheckFailed(error: Error)
}
// MARK: - Custom Update Check Result
struct CustomUpdateCheckResult {
let hasUpdate: Bool
let latestVersion: String?
let currentVersion: String
let downloadURL: String?
let releaseNotes: String?
}
// MARK: - Update Manager
class UpdateManager: NSObject {
// MARK: - Properties
static let shared = UpdateManager()
private var updaterController: SPUStandardUpdaterController?
weak var delegate: UpdateManagerDelegate?
private var isManualCheck = false // Track if this is a manual check
private let feedURL = "https://git.plet.i234.me/Nick/shotscreen/raw/branch/main/appcast.xml"
// MARK: - Initialization
override init() {
super.init()
setupUpdater()
}
// MARK: - Setup
private func setupUpdater() {
print("🔄 UPDATE: Initializing Sparkle updater...")
// Check if we're in a development build
let isDevelopmentBuild = isRunningInDevelopment()
print("🔧 UPDATE: Development build detected: \(isDevelopmentBuild)")
// Additional debug info
print("🔍 UPDATE: Current app version: \(currentVersion)")
print("🔍 UPDATE: Current build number: \(currentBuildNumber)")
print("🔍 UPDATE: Bundle path: \(Bundle.main.bundlePath)")
print("🔍 UPDATE: Bundle URL: \(Bundle.main.bundleURL)")
print("🔍 UPDATE: Executable path: \(CommandLine.arguments[0])")
// Initialize Sparkle with standard configuration (for automatic updates only)
updaterController = SPUStandardUpdaterController(
startingUpdater: !isDevelopmentBuild, // Don't auto-start for dev builds
updaterDelegate: self,
userDriverDelegate: nil // Use standard driver for automatic checks
)
// Configure the updater
if let updater = updaterController?.updater {
// Configure automatic updates based on user settings and build type
let automaticUpdatesEnabled = !isDevelopmentBuild && SettingsManager.shared.automaticUpdates
updater.automaticallyChecksForUpdates = automaticUpdatesEnabled
updater.automaticallyDownloadsUpdates = false // Always ask user
print("✅ UPDATE: Sparkle updater initialized successfully")
print("🔗 UPDATE: Feed URL configured via Info.plist: \(feedURL)")
print("🤖 UPDATE: Automatic checks: \(updater.automaticallyChecksForUpdates)")
print("⚙️ UPDATE: SettingsManager automatic updates: \(SettingsManager.shared.automaticUpdates)")
print("🚀 UPDATE: Updater started: \(!isDevelopmentBuild)")
// Force an immediate update check if this is a manual debug
print("🔍 UPDATE: Last update check date: \(updater.lastUpdateCheckDate?.description ?? "Never")")
} else {
print("❌ UPDATE: Failed to access Sparkle updater")
}
}
// MARK: - Development Detection
private func isRunningInDevelopment() -> Bool {
// Check if we're running from .build directory (swift run)
let executablePath = CommandLine.arguments[0]
let isDevelopment = executablePath.contains(".build") ||
executablePath.contains("DerivedData") ||
Bundle.main.bundleURL.pathExtension != "app"
print("🔍 UPDATE: Executable path: \(executablePath)")
print("🔍 UPDATE: Bundle URL: \(Bundle.main.bundleURL)")
return isDevelopment
}
// MARK: - Public Interface
/// Manually check for updates (triggered by user) - USE SPARKLE'S NATIVE APPROACH
func checkForUpdates() {
print("🔍 UPDATE: === checkForUpdates() START ===")
print("🔍 UPDATE: updaterController exists: \(updaterController != nil)")
if let controller = updaterController {
print("🔍 UPDATE: updaterController.updater exists: true")
}
guard let updater = updaterController?.updater else {
print("❌ UPDATE: Updater not available - showing fallback")
showManualUpdateFallback()
return
}
print("🔍 UPDATE: Manual update check triggered (using Sparkle's native method)")
print("🔍 UPDATE: Current version from bundle: \(currentVersion)")
print("🔍 UPDATE: Current build number: \(currentBuildNumber)")
print("🔍 UPDATE: Feed URL: \(feedURL)")
print("🔍 UPDATE: Automatic checks enabled: \(updater.automaticallyChecksForUpdates)")
print("🔍 UPDATE: Last check date: \(updater.lastUpdateCheckDate?.description ?? "Never")")
isManualCheck = true
delegate?.updateCheckDidStart()
print("🔍 UPDATE: About to call updater.checkForUpdates()")
// Use Sparkle's own checkForUpdates method (this should work reliably)
updater.checkForUpdates()
print("🔍 UPDATE: updater.checkForUpdates() called successfully")
}
// MARK: - Custom Update Check Implementation (REMOVED - now only used as fallback)
private func performCustomUpdateCheckFallback() {
guard let url = URL(string: feedURL) else {
print("❌ UPDATE: Invalid feed URL")
delegate?.updateCheckFailed(error: NSError(domain: "UpdateManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid feed URL"]))
isManualCheck = false
return
}
print("🔍 UPDATE: Performing fallback update check from: \(feedURL)")
// Create URL session
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
DispatchQueue.main.async {
self?.handleCustomUpdateCheckResponse(data: data, response: response, error: error)
}
}
task.resume()
}
private func handleCustomUpdateCheckResponse(data: Data?, response: URLResponse?, error: Error?) {
defer {
isManualCheck = false
delegate?.updateCheckDidFinish()
}
if let error = error {
print("❌ UPDATE: Network error: \(error.localizedDescription)")
delegate?.updateCheckFailed(error: error)
return
}
guard let data = data else {
print("❌ UPDATE: No data received")
let error = NSError(domain: "UpdateManager", code: -2, userInfo: [NSLocalizedDescriptionKey: "No data received"])
delegate?.updateCheckFailed(error: error)
return
}
print("✅ UPDATE: Received appcast data (\(data.count) bytes)")
// Parse the appcast XML
parseAppcastAndCheckForUpdates(data: data)
}
private func parseAppcastAndCheckForUpdates(data: Data) {
do {
let xml = try XMLDocument(data: data, options: [])
// Find the latest item in the appcast
guard let latestItem = xml.rootElement()?.elements(forName: "channel").first?.elements(forName: "item").first else {
print("❌ UPDATE: No items found in appcast")
delegate?.noUpdateAvailable()
return
}
// Extract version information
guard let enclosure = latestItem.elements(forName: "enclosure").first,
let versionAttribute = enclosure.attribute(forName: "sparkle:version"),
let latestVersionString = versionAttribute.stringValue else {
print("❌ UPDATE: Could not extract version from appcast")
delegate?.noUpdateAvailable()
return
}
let currentVersionString = currentVersion
print("🔍 UPDATE: Current version: \(currentVersionString)")
print("🔍 UPDATE: Latest version: \(latestVersionString)")
// Compare versions
let hasUpdate = compareVersions(current: currentVersionString, latest: latestVersionString)
if hasUpdate {
print("🎉 UPDATE: New version available!")
// Create a mock SUAppcastItem for compatibility
if let mockItem = createMockAppcastItem(from: latestItem, version: latestVersionString) {
delegate?.updateAvailable(mockItem)
} else {
// Fallback to custom update available notification
showCustomUpdateAvailable(latestVersion: latestVersionString, currentVersion: currentVersionString)
}
} else {
print("️ UPDATE: No update available (current: \(currentVersionString), latest: \(latestVersionString))")
delegate?.noUpdateAvailable()
}
} catch {
print("❌ UPDATE: XML parsing error: \(error.localizedDescription)")
delegate?.updateCheckFailed(error: error)
}
}
private func compareVersions(current: String, latest: String) -> Bool {
// Handle "Unknown" version (development builds)
if current == "Unknown" {
print("🔍 UPDATE: Skipping update check for development build")
return false // Don't offer updates for development builds
}
print("🔍 UPDATE: Comparing versions - Current: '\(current)' vs Latest: '\(latest)'")
// Simple version comparison (assuming semantic versioning like "1.0", "1.1", etc.)
let currentComponents = current.split(separator: ".").compactMap { Int($0) }
let latestComponents = latest.split(separator: ".").compactMap { Int($0) }
print("🔍 UPDATE: Current components: \(currentComponents)")
print("🔍 UPDATE: Latest components: \(latestComponents)")
// Pad arrays to same length
let maxLength = max(currentComponents.count, latestComponents.count)
var currentPadded = currentComponents
var latestPadded = latestComponents
while currentPadded.count < maxLength { currentPadded.append(0) }
while latestPadded.count < maxLength { latestPadded.append(0) }
print("🔍 UPDATE: Current padded: \(currentPadded)")
print("🔍 UPDATE: Latest padded: \(latestPadded)")
// Compare component by component
for i in 0..<maxLength {
print("🔍 UPDATE: Comparing index \(i): \(latestPadded[i]) vs \(currentPadded[i])")
if latestPadded[i] > currentPadded[i] {
print("🎉 UPDATE: Found newer version at component \(i): \(latestPadded[i]) > \(currentPadded[i])")
return true
} else if latestPadded[i] < currentPadded[i] {
print("⚠️ UPDATE: Latest version is older at component \(i): \(latestPadded[i]) < \(currentPadded[i])")
return false
}
}
print("️ UPDATE: Versions are equal")
return false // Versions are equal
}
private func createMockAppcastItem(from xmlElement: XMLElement, version: String) -> SUAppcastItem? {
// This is a bit tricky as SUAppcastItem is not easily creatable
// For now, we'll use our custom update available method
return nil
}
private func showCustomUpdateAvailable(latestVersion: String, currentVersion: String) {
DispatchQueue.main.async { [weak self] in
let alert = NSAlert()
alert.messageText = "Update Available! 🎉"
alert.informativeText = """
ShotScreen \(latestVersion) is now available.
You have version \(currentVersion).
Would you like to visit the releases page to download it?
"""
alert.addButton(withTitle: "Download Update")
alert.addButton(withTitle: "Skip This Version")
alert.addButton(withTitle: "Remind Me Later")
alert.alertStyle = .informational
// Set app icon
if let appIcon = NSApplication.shared.applicationIconImage {
alert.icon = appIcon
}
let response = alert.runModal()
switch response {
case .alertFirstButtonReturn:
print("✅ UPDATE: User chose to download update")
if let url = URL(string: "https://git.plet.i234.me/Nick/shotscreen/releases") {
NSWorkspace.shared.open(url)
}
case .alertSecondButtonReturn:
print("❌ UPDATE: User chose to skip this version")
case .alertThirdButtonReturn:
print("⏰ UPDATE: User chose to be reminded later")
default:
break
}
}
}
/// Update automatic update settings
func updateAutomaticUpdateSettings() {
guard let updater = updaterController?.updater else { return }
let automaticUpdates = SettingsManager.shared.automaticUpdates
updater.automaticallyChecksForUpdates = automaticUpdates
print("⚙️ UPDATE: Automatic updates \(automaticUpdates ? "enabled" : "disabled")")
}
/// Check if updater is available
var isUpdaterAvailable: Bool {
return updaterController?.updater != nil
}
/// Get current app version
var currentVersion: String {
return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Unknown"
}
/// Get current build number
var currentBuildNumber: String {
return Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "Unknown"
}
// MARK: - Private Methods
private func showManualUpdateFallback() {
print("🔄 UPDATE: Showing manual update fallback")
let alert = NSAlert()
alert.messageText = "Check for Updates"
alert.informativeText = "Automatic update checking is currently unavailable. You can check for updates manually on our releases page."
alert.addButton(withTitle: "Open Releases Page")
alert.addButton(withTitle: "Cancel")
alert.alertStyle = .informational
let response = alert.runModal()
if response == .alertFirstButtonReturn {
if let url = URL(string: "https://git.plet.i234.me/Nick/shotscreen/releases") {
NSWorkspace.shared.open(url)
}
}
}
// MARK: - Debug Info
func printDebugInfo() {
print("\n🔍 UPDATE DEBUG INFO:")
print("📱 Current Version: \(currentVersion)")
print("🔢 Build Number: \(currentBuildNumber)")
print("🔗 Feed URL: \(feedURL)")
print("⚡ Updater Available: \(isUpdaterAvailable)")
print("🤖 Automatic Updates: \(SettingsManager.shared.automaticUpdates)")
if let updater = updaterController?.updater {
print("📅 Last Check: \(updater.lastUpdateCheckDate?.description ?? "Never")")
}
print("🔍 UPDATE DEBUG INFO END\n")
}
/// Force a debug update check - bypasses development detection temporarily
func forceDebugUpdateCheck() {
print("🔧 DEBUG: Forcing update check (bypassing development detection)")
// Temporarily create a new updater controller that ignores development mode
let debugController = SPUStandardUpdaterController(
startingUpdater: true, // Always start for debug
updaterDelegate: self,
userDriverDelegate: nil
)
let debugUpdater = debugController.updater
print("🔧 DEBUG: Debug updater created successfully")
debugUpdater.automaticallyChecksForUpdates = false
debugUpdater.automaticallyDownloadsUpdates = false
isManualCheck = true
delegate?.updateCheckDidStart()
print("🔧 DEBUG: Starting forced update check...")
debugUpdater.checkForUpdates()
}
}
// MARK: - SPUUpdaterDelegate (automatic updates only - manual updates use custom implementation)
extension UpdateManager: @preconcurrency SPUUpdaterDelegate {
func updater(_ updater: SPUUpdater, didFinishUpdateCycleFor updateCheck: SPUUpdateCheck, error: Error?) {
// This is only called for automatic background checks now (not manual checks)
print("✅ UPDATE: Automatic update check completed")
if let error = error {
print("❌ UPDATE: Automatic update check failed: \(error.localizedDescription)")
}
// Let Sparkle handle everything - no delegate calls to prevent UI interference
isManualCheck = false
}
func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) {
print("🎉 UPDATE: Sparkle found valid update!")
print("🔍 UPDATE: Update version: \(item.displayVersionString)")
print("🔍 UPDATE: Current version: \(currentVersion)")
print("🔍 UPDATE: Update info: \(item.infoURL?.absoluteString ?? "None")")
print("🔍 UPDATE: Was manual check: \(isManualCheck)")
// Let Sparkle handle the UI completely - don't trigger additional dialogs
// Just notify delegate for internal state tracking (no additional UI)
isManualCheck = false
// delegate?.updateAvailable(item) // Commented out to prevent double UI
}
func updaterDidNotFindUpdate(_ updater: SPUUpdater) {
print("️ UPDATE: No updates found by Sparkle")
print("🔍 UPDATE: Current version: \(currentVersion)")
print("🔍 UPDATE: Feed URL used: \(feedURL)")
print("🔍 UPDATE: Was manual check: \(isManualCheck)")
// Let Sparkle handle the "no update" UI completely - don't add our own popup
// Sparkle already shows its own "You're up to date!" dialog for manual checks
isManualCheck = false
// delegate?.noUpdateAvailable() // Commented out to prevent multiple dialogs
}
func updater(_ updater: SPUUpdater, willInstallUpdate item: SUAppcastItem) {
print("📦 UPDATE: Will install update: \(item.displayVersionString)")
}
func updater(_ updater: SPUUpdater, didAbortWithError error: Error) {
print("💥 UPDATE: Update aborted with error: \(error.localizedDescription)")
// For automatic updates, we don't need to notify the delegate of failures
// since they're silent background operations
}
}
@@ -0,0 +1,998 @@
import AppKit
import ScreenCaptureKit
import SwiftUI
import UniformTypeIdentifiers
import ObjectiveC
// MARK: - Window Capture Manager
@available(macOS 12.3, *)
class WindowCaptureManager: NSObject, ObservableObject {
// MARK: - Properties
weak var screenshotApp: ScreenshotApp?
var determinedMainScreen: NSScreen! // Stores the true main screen for the current selection session
// Available content for capture
@Published var availableWindows: [SCWindow] = []
@Published var availableDisplays: [SCDisplay] = []
@Published var isCapturing = false
@Published var captureError: String?
// Window selection state
@Published var isWindowSelectionActive = false
private var windowSelectionOverlay: WindowSelectionOverlay?
private var globalMouseMonitor: Any?
// MARK: - Initialization
override init() {
super.init()
}
convenience init(screenshotApp: ScreenshotApp) {
self.init()
self.screenshotApp = screenshotApp
}
// MARK: - Content Discovery
func refreshAvailableContent() async {
print("🔄 Refreshing available content for window capture...")
do {
let shareableContent = try await SCShareableContent.current
await updateAvailableContent(shareableContent)
print("✅ Content refreshed successfully")
} catch {
await handleContentRefreshError(error)
}
}
// MARK: - Private Helper Methods for Content Discovery
private func updateAvailableContent(_ shareableContent: SCShareableContent) async {
let filteredWindows = filterShareableWindows(shareableContent.windows)
let validDisplays = shareableContent.displays.filter { $0.width > 0 && $0.height > 0 }
await MainActor.run {
self.availableWindows = filteredWindows
self.availableDisplays = validDisplays
self.captureError = nil
}
logContentSummary(windows: filteredWindows, displays: validDisplays)
}
private func filterShareableWindows(_ windows: [SCWindow]) -> [SCWindow] {
return windows.filter { window in
// Filter criteria for capturable windows
guard window.isOnScreen,
window.frame.width > 50,
window.frame.height > 50,
let app = window.owningApplication,
app.applicationName != "WindowServer",
app.applicationName != "Dock" else {
return false
}
// Exclude our own app's windows
if let ourBundleID = Bundle.main.bundleIdentifier,
app.bundleIdentifier == ourBundleID {
return false
}
return true
}
}
private func logContentSummary(windows: [SCWindow], displays: [SCDisplay]) {
print("📊 Content Summary:")
print(" Windows: \(windows.count) capturable windows found")
print(" Displays: \(displays.count) displays found")
if windows.count > 0 {
print("🪟 Sample windows:")
for (index, window) in windows.prefix(5).enumerated() {
let app = window.owningApplication?.applicationName ?? "Unknown"
let title = window.title?.isEmpty == false ? window.title! : "No Title"
print(" \(index + 1). \(title) (\(app)) - Layer: \(window.windowLayer)")
}
if windows.count > 5 {
print(" ... and \(windows.count - 5) more windows")
}
}
}
private func handleContentRefreshError(_ error: Error) async {
await MainActor.run {
self.captureError = "Failed to get shareable content: \(error.localizedDescription)"
print("❌ WindowCaptureManager Error: \(error)")
}
}
// MARK: - Window Selection Mode
func activateWindowSelectionMode() {
print("🎬 ACTIVATE: Starting window selection mode...")
print("🪟 Activating window selection mode")
// Determine the actual main screen (with origin 0,0) for consistent coordinate calculations
self.determinedMainScreen = NSScreen.screens.first { $0.frame.origin == .zero }
if self.determinedMainScreen == nil {
print("⚠️ CRITICAL: Could not find screen with origin (0,0). Falling back to default NSScreen.main.")
self.determinedMainScreen = NSScreen.main! // Fallback, though this might be the unreliable one
}
print("✨ Using determined main screen for this session: \(determinedMainScreen.customLocalizedName) with frame \(determinedMainScreen.frame)")
print("🎬 ACTIVATE: Starting async task to refresh content and show overlay...")
Task {
print("🎬 TASK: Inside async task, refreshing content...")
await refreshAvailableContent()
await MainActor.run {
print("🎬 TASK: Content refreshed, setting flags and showing overlay...")
self.isWindowSelectionActive = true
self.showWindowSelectionOverlay()
print("🎬 TASK: Window selection overlay should now be shown")
}
}
}
// MARK: - Window Selection Mode Deactivation
func deactivateWindowSelectionMode() {
print("🪟 Deactivating window selection mode")
isWindowSelectionActive = false
hideWindowSelectionOverlay()
// Remove any mouse monitors
if let monitor = globalMouseMonitor {
NSEvent.removeMonitor(monitor)
globalMouseMonitor = nil
}
}
private func showWindowSelectionOverlay() {
// Create overlay window that shows available windows
let overlay = WindowSelectionOverlay(windowCaptureManager: self)
overlay.show()
windowSelectionOverlay = overlay
}
private func hideWindowSelectionOverlay() {
windowSelectionOverlay?.hide()
windowSelectionOverlay = nil
}
private func setupWindowSelectionMonitoring() {
globalMouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown]) { [weak self] event in
guard let self = self, self.isWindowSelectionActive else { return }
let location = NSEvent.mouseLocation
self.handleWindowSelection(at: location)
}
}
private func removeWindowSelectionMonitoring() {
if let monitor = globalMouseMonitor {
NSEvent.removeMonitor(monitor)
globalMouseMonitor = nil
}
}
private func handleWindowSelection(at location: NSPoint) {
// HERSTEL: Zoek naar een window
if let window = findWindowAt(location: location) {
let windowTitleForPrint = window.title ?? "Untitled"
print("🎯 Window selected: \(windowTitleForPrint) at \(location)")
deactivateWindowSelectionMode()
Task {
await captureWindow(window) // Gebruik de captureWindow methode
}
} else {
print(" No window found at \(location), canceling")
deactivateWindowSelectionMode()
}
}
// MARK: - Window Detection
func findWindowAt(location: NSPoint) -> SCWindow? {
NSLog("🎯 Finding window at global location: %@", NSStringFromPoint(location))
// Step 1: Validate main screen reference
guard let mainScreenRef = validateMainScreenReference() else {
return nil
}
// Step 2: Find the screen containing the mouse location
guard let mouseScreen = findScreenContaining(location: location) else {
return nil
}
// Step 3: Search for window at location
return searchWindowsAt(location: location, mouseScreen: mouseScreen, mainScreenRef: mainScreenRef)
}
// MARK: - Private Helper Methods for Window Detection
private func validateMainScreenReference() -> NSScreen? {
guard let mainScreenRef = self.determinedMainScreen else {
NSLog("❌ No determined main screen available for findWindowAt")
return nil
}
return mainScreenRef
}
private func findScreenContaining(location: NSPoint) -> NSScreen? {
for screen in NSScreen.screens {
if screen.frame.contains(location) {
NSLog("🎯 Mouse is on NSScreen: '%@' (Frame: %@)", screen.customLocalizedName, NSStringFromRect(screen.frame))
return screen
}
}
NSLog("❌ Mouse location not on any NSScreen: %@", NSStringFromPoint(location))
return nil
}
private func searchWindowsAt(location: NSPoint, mouseScreen: NSScreen, mainScreenRef: NSScreen) -> SCWindow? {
// Sort windows by layer, so frontmost windows are checked first
let sortedWindows = availableWindows.sorted { $0.windowLayer > $1.windowLayer }
NSLog("🎯 Checking \(sortedWindows.count) sorted windows.")
for window in sortedWindows {
if let foundWindow = checkWindowAtLocation(window: window, location: location,
mouseScreen: mouseScreen, mainScreenRef: mainScreenRef) {
return foundWindow
}
}
NSLog(" ❌ No window found at global location: %@ on screen %@", NSStringFromPoint(location), mouseScreen.customLocalizedName)
return nil
}
private func checkWindowAtLocation(window: SCWindow, location: NSPoint,
mouseScreen: NSScreen, mainScreenRef: NSScreen) -> SCWindow? {
let scWinFrame = window.frame
let globalWindowFrame = self.convertSCWindowFrameToGlobal(scWinFrame, mainScreen: mainScreenRef)
// Check 1: Does the window intersect with the screen the mouse is on?
guard mouseScreen.frame.intersects(globalWindowFrame) else {
return nil
}
// Check 2: Does the global window frame contain the mouse location?
guard globalWindowFrame.contains(location) else {
return nil
}
NSLog(" 🎉 FOUND window by global coords: '%@' (App: %@) on screen '%@'",
window.title ?? "Untitled",
window.owningApplication?.applicationName ?? "Unknown",
mouseScreen.customLocalizedName)
return window
}
// MARK: - Window Capture Methods
/// Capture a specific window by SCWindow object
func captureWindow(_ window: SCWindow) async {
print("📸 WindowCaptureManager: Capturing window: \(window.title ?? "Untitled") using ScreenCaptureKitProvider")
// Step 1: Initialize capture state
await initializeCaptureState()
// Step 2: Validate screen capture provider
guard let provider = validateScreenCaptureProvider() else {
await handleCaptureError("ScreenCaptureProvider not available.", code: 3)
return
}
// Step 3: Perform capture
await performWindowCapture(window: window, provider: provider)
}
// MARK: - Private Helper Methods for Window Capture
private func initializeCaptureState() async {
await MainActor.run {
isCapturing = true
captureError = nil
}
}
private func validateScreenCaptureProvider() -> ScreenCaptureKitProvider? {
return self.screenshotApp?.screenCaptureProvider
}
private func performWindowCapture(window: SCWindow, provider: ScreenCaptureKitProvider) async {
if let image = await provider.captureWindow(window: window) {
await handleSuccessfulCapture(image: image)
} else {
await handleCaptureError("Failed to capture window with ScreenCaptureKitProvider.", code: 2)
}
}
private func handleSuccessfulCapture(image: NSImage) async {
await MainActor.run {
self.isCapturing = false
self.screenshotApp?.processCapture(image: image)
self.deactivateWindowSelectionMode()
print("✅ Window captured successfully and selection mode deactivated.")
}
}
private func handleCaptureError(_ description: String, code: Int) async {
await MainActor.run {
self.isCapturing = false
self.captureError = NSError(domain: "WindowCaptureError", code: code,
userInfo: [NSLocalizedDescriptionKey: description]).localizedDescription
self.deactivateWindowSelectionMode()
print("Error: \(description)")
}
}
// MARK: - Core Graphics Capture Method (Old - Replaced by ScreenCaptureKitProvider)
/*
private func captureWindowWithCoreGraphics(_ window: SCWindow) async {
let windowID = window.windowID
let imageRef = CGWindowListCreateImage(.null, .optionIncludingWindow, windowID, .bestResolution)
guard let cgImage = imageRef else {
await MainActor.run {
self.captureError = "Failed to capture window"
self.isCapturing = false
}
return
}
let nsImage = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height))
await MainActor.run {
self.isCapturing = false
// Process the captured image through the main app
self.screenshotApp?.processWindowCapture(image: nsImage, windowTitle: window.title)
}
}
*/
/// Capture window by application name
func captureWindowByApp(appName: String) async {
await refreshAvailableContent()
guard let window = availableWindows.first(where: {
$0.owningApplication?.applicationName == appName
}) else {
await MainActor.run {
self.captureError = "No window found for app: \(appName)"
}
return
}
await captureWindow(window)
}
/// Capture frontmost window
func captureFrontmostWindow() async {
await refreshAvailableContent()
// Get the frontmost window (highest window level)
guard let frontmostWindow = availableWindows.max(by: { $0.windowLayer < $1.windowLayer }) else {
await MainActor.run {
self.captureError = "No frontmost window found"
}
return
}
await captureWindow(frontmostWindow)
}
/// Capture window containing a specific point
func captureWindowAt(point: NSPoint) async {
await refreshAvailableContent()
guard let window = findWindowAt(location: point) else {
await MainActor.run {
self.captureError = "No window found at specified location"
}
return
}
await captureWindow(window)
}
// MARK: - Settings Integration
func getWindowCaptureSettings() -> WindowCaptureSettings {
return WindowCaptureSettings(
includeCursor: UserDefaults.standard.bool(forKey: "windowCaptureIncludeCursor"),
showSelectionUI: UserDefaults.standard.bool(forKey: "windowCaptureShowSelectionUI")
)
}
func saveWindowCaptureSettings(_ settings: WindowCaptureSettings) {
UserDefaults.standard.set(settings.includeCursor, forKey: "windowCaptureIncludeCursor")
UserDefaults.standard.set(settings.showSelectionUI, forKey: "windowCaptureShowSelectionUI")
}
// MARK: - Coordinate Conversion Helper
// Converts an SCWindow.frame (origin top-left of main screen, Y-down) to Global coordinates (origin bottom-left of main screen, Y-up)
func convertSCWindowFrameToGlobal(_ scWindowFrame: CGRect, mainScreen: NSScreen) -> CGRect {
let globalX = scWindowFrame.origin.x
// Y of top edge of scWindow in Global Y-up (from main screen bottom)
let globalY_topEdge = mainScreen.frame.height - scWindowFrame.origin.y
// Y of bottom edge of scWindow in Global Y-up (this becomes the origin for the CGRect)
let globalY_bottomEdge_forOrigin = globalY_topEdge - scWindowFrame.height
return CGRect(x: globalX, y: globalY_bottomEdge_forOrigin, width: scWindowFrame.width, height: scWindowFrame.height)
}
// MARK: - Window Detection (Wordt Display Detection)
func findDisplayAt(location: NSPoint) -> SCDisplay? {
NSLog("🎯 Finding display at global location: %@", NSStringFromPoint(location))
// We gebruiken de beschikbare SCDisplay objecten direct.
// Hun frames zijn in pixels, oorsprong linksboven van *dat specifieke display*.
// Om te checken of de muis (globale punten, oorsprong linksonder hoofdmenu) binnen een display valt,
// moeten we de SCDisplay.frame converteren naar globale punten, of de muislocatie naar de coördinaten van elk display.
// Makkelijker: gebruik NSScreen.screens, vind de NSScreen waar de muis op is, en zoek dan de SCDisplay met dezelfde displayID.
var currentMouseNSScreen: NSScreen?
for s in NSScreen.screens {
if s.frame.contains(location) {
currentMouseNSScreen = s
break
}
}
guard let mouseNSScreen = currentMouseNSScreen else {
NSLog("❌ Mouse location %@ not on any NSScreen.", NSStringFromPoint(location))
return nil
}
NSLog("🎯 Mouse is on NSScreen: '%@' (Frame: %@), DisplayID: \(mouseNSScreen.displayID)", mouseNSScreen.customLocalizedName, NSStringFromRect(mouseNSScreen.frame))
// Zoek de SCDisplay die overeenkomt met deze NSScreen
if let matchedDisplay = availableDisplays.first(where: { $0.displayID == mouseNSScreen.displayID }) {
NSLog(" 🎉 FOUND display by ID match: DisplayID \(matchedDisplay.displayID)")
return matchedDisplay
}
NSLog(" ❌ No SCDisplay found matching NSScreen with DisplayID \(mouseNSScreen.displayID) at global location: %@", NSStringFromPoint(location))
return nil
}
// MARK: - Window Capture Methods (Wordt Display Capture Methods)
/// Capture a specific display by SCDisplay object
func captureDisplay(_ display: SCDisplay) async {
print("🖥️ WindowCaptureManager: Capturing display ID \(display.displayID) using ScreenCaptureKitProvider")
// Converteer SCDisplay naar NSScreen
guard let targetNSScreen = NSScreen.screens.first(where: { $0.displayID == display.displayID }) else {
await MainActor.run {
self.isCapturing = false
self.captureError = NSError(domain: "DisplayCaptureError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Could not find NSScreen matching SCDisplay ID \(display.displayID)."]).localizedDescription
self.deactivateWindowSelectionMode()
print("Error: Could not find NSScreen for SCDisplay ID \(display.displayID).")
}
return
}
print("🖥️ Corresponderende NSScreen gevonden: \(targetNSScreen.customLocalizedName)")
await MainActor.run {
isCapturing = true
captureError = nil
}
if let provider = self.screenshotApp?.screenCaptureProvider {
// ScreenCaptureKitProvider.captureScreen verwacht een NSScreen
// De excludingWindows parameter is optioneel en wordt hier nil gelaten,
// omdat we het hele scherm capturen en desktop icons apart worden behandeld in de provider.
if let image = await provider.captureScreen(screen: targetNSScreen, excludingWindows: nil) {
await MainActor.run {
self.isCapturing = false
self.screenshotApp?.processCapture(image: image)
self.deactivateWindowSelectionMode()
print("✅ Display captured successfully and selection mode deactivated.")
}
} else {
await MainActor.run {
self.isCapturing = false
self.captureError = NSError(domain: "DisplayCaptureError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to capture display with ScreenCaptureKitProvider."]).localizedDescription
self.deactivateWindowSelectionMode()
print("Error: Failed to capture display ID \(display.displayID) with ScreenCaptureKitProvider.")
}
}
} else {
await MainActor.run {
self.isCapturing = false
self.captureError = NSError(domain: "DisplayCaptureError", code: 3, userInfo: [NSLocalizedDescriptionKey: "ScreenCaptureProvider not available."]).localizedDescription
self.deactivateWindowSelectionMode()
print("Error: ScreenCaptureProvider not available for display capture.")
}
}
}
}
// MARK: - Window Capture Settings
struct WindowCaptureSettings {
var includeCursor: Bool = true
var showSelectionUI: Bool = true
}
// MARK: - Window Selection Overlay
@available(macOS 12.3, *)
class WindowSelectionOverlay: NSObject {
private var windowCaptureManager: WindowCaptureManager
private var overlayWindows: [NSWindow] = [] // Dit zijn de semi-transparante overlay windows per scherm
private var globalMouseMonitor: Any?
init(windowCaptureManager: WindowCaptureManager) {
self.windowCaptureManager = windowCaptureManager
super.init()
}
func show() {
print("🎬 OVERLAY SHOW: Starting to create overlays...")
hide() // Clean up any existing overlays
print("🪟 WindowCaptureManager: Starting window selection on \(NSScreen.screens.count) screens")
for (index, screen) in NSScreen.screens.enumerated() {
print(" Screen \(index): \(screen.customLocalizedName) - Frame: \(screen.frame)")
}
// Create a separate overlay window for each screen
for (index, screen) in NSScreen.screens.enumerated() {
print("🎬 OVERLAY SHOW: Creating overlay for screen \(index): \(screen.customLocalizedName)")
let overlayWindow = createOverlayWindow(for: screen)
overlayWindows.append(overlayWindow)
print("🎬 OVERLAY SHOW: Window created, ordering front...")
// IMPROVED: Proper window ordering and activation
overlayWindow.orderFrontRegardless()
overlayWindow.makeKeyAndOrderFront(nil)
print("🎬 OVERLAY SHOW: Window ordered front, checking content view...")
// Force the content view to display immediately
if let contentView = overlayWindow.contentView {
print("🎬 OVERLAY SHOW: Content view found, forcing display...")
contentView.needsDisplay = true
contentView.displayIfNeeded()
print("🎨 FORCED DISPLAY for screen \(screen.customLocalizedName)")
} else {
print("❌ OVERLAY SHOW: NO CONTENT VIEW found for screen \(screen.customLocalizedName)")
}
}
print("🎬 OVERLAY SHOW: All overlays created, setting up mouse monitoring...")
// Setup global mouse monitoring for all screens
setupGlobalMouseMonitoring()
// Force app activation
NSApp.activate(ignoringOtherApps: true)
print("🪟 Window selection overlay shown on \(overlayWindows.count) screens")
// ADDITIONAL: Force a refresh after a short delay
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
print("🎬 DELAYED REFRESH: Starting...")
for (index, window) in self.overlayWindows.enumerated() {
if let contentView = window.contentView {
contentView.needsDisplay = true
print("🎨 DELAYED REFRESH for window \(index)")
} else {
print("❌ DELAYED REFRESH: NO CONTENT VIEW for window \(index)")
}
}
}
// Debug: List available windows with their positions
print("🪟 Available windows for capture:")
for (index, window) in windowCaptureManager.availableWindows.enumerated() {
let frame = window.frame
let app = window.owningApplication?.applicationName ?? "Unknown"
print(" \(index): \(window.title ?? "Untitled") (\(app)) - Frame: \(frame)")
}
}
func hide() {
// Remove all overlay windows
for window in overlayWindows {
window.orderOut(nil)
}
overlayWindows.removeAll()
// Remove global mouse monitor
if let monitor = globalMouseMonitor {
NSEvent.removeMonitor(monitor)
globalMouseMonitor = nil
}
print("🪟 Window selection overlay hidden")
}
private func createOverlayWindow(for screen: NSScreen) -> NSWindow {
let window = NSWindow(
contentRect: screen.frame,
styleMask: [.borderless, .fullSizeContentView],
backing: .buffered,
defer: false
)
// FIXED: Window configuration for proper drawing and event handling
window.backgroundColor = NSColor.clear // Clear background so we can draw our own
window.isOpaque = false
window.level = NSWindow.Level.floating // Lower level but still above normal windows
window.ignoresMouseEvents = false
window.acceptsMouseMovedEvents = true
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .ignoresCycle]
// CRITICAL: Ensure window can receive events and display properly
window.displaysWhenScreenProfileChanges = true
window.animationBehavior = .none
window.hidesOnDeactivate = false
// IMPORTANT: Enable automatic invalidation for proper drawing
window.invalidateShadow()
window.viewsNeedDisplay = true
// Create content view for this screen
let overlayView = WindowSelectionOverlayView(
windowCaptureManager: windowCaptureManager,
screen: screen
)
// CRITICAL: Properly configure the view for drawing
overlayView.wantsLayer = false // Use direct drawing for better control
overlayView.needsDisplay = true
window.contentView = overlayView
// Position window exactly on this screen
window.setFrame(screen.frame, display: true) // Force display update
print("🪟 Created overlay window for screen: \(screen.customLocalizedName) at level \(window.level.rawValue)")
print("🪟 Window frame: \(window.frame)")
print("🪟 Content view frame: \(overlayView.frame)")
print("🪟 Content view needsDisplay: \(overlayView.needsDisplay)")
return window
}
private func setupGlobalMouseMonitoring() {
globalMouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .keyDown]) { [weak self] event in
guard let self = self else { return }
if event.type == .leftMouseDown {
let location = NSEvent.mouseLocation
print("🎯 Global mouse click at: \(location) for window capture")
print("🔥 DEBUG: Available windows: \(self.windowCaptureManager.availableWindows.count)")
// 🔥 FIXED: In window capture mode, any click should capture the window
print("🪟 Click detected in window capture mode - capturing window")
self.handleMouseClick(at: location)
} else if event.type == .keyDown && event.keyCode == 53 { // ESC
print("⌨️ ESC key pressed, canceling window/display capture")
self.windowCaptureManager.deactivateWindowSelectionMode()
}
}
print("🎯 Global mouse monitoring setup for window capture")
}
private func handleMouseClick(at location: NSPoint) {
// HERSTEL: Zoek naar een window
if let window = windowCaptureManager.findWindowAt(location: location) {
let selectedWindowTitle = window.title ?? "Untitled"
print("🎯 Window selected: \(selectedWindowTitle) at \(location)")
windowCaptureManager.deactivateWindowSelectionMode()
Task {
await windowCaptureManager.captureWindow(window) // Gebruik de captureWindow methode
}
} else {
print(" No window found at \(location), canceling")
windowCaptureManager.deactivateWindowSelectionMode()
}
}
}
// MARK: - Window Selection Overlay View
@available(macOS 12.3, *)
class WindowSelectionOverlayView: NSView {
private var windowCaptureManager: WindowCaptureManager
private var screen: NSScreen // Het NSScreen object waar deze view op getekend wordt
private var hoveredWindow: SCWindow? // HERSTEL: terug naar SCWindow
private var trackingArea: NSTrackingArea?
init(windowCaptureManager: WindowCaptureManager, screen: NSScreen) {
self.windowCaptureManager = windowCaptureManager
self.screen = screen
super.init(frame: NSRect(origin: .zero, size: screen.frame.size))
self.wantsLayer = false
self.layerContentsRedrawPolicy = .onSetNeedsDisplay
self.autoresizingMask = [.width, .height]
self.translatesAutoresizingMaskIntoConstraints = true
NSLog("🎨✅ VIEW INIT for screen: %@, Frame: %@, Bounds: %@", screen.customLocalizedName, NSStringFromRect(self.frame), NSStringFromRect(self.bounds))
setupTrackingArea()
self.needsDisplay = true
DispatchQueue.main.async {
NSLog("🎨✅ VIEW INIT DELAYED DISPLAY for screen: %@", self.screen.customLocalizedName)
self.needsDisplay = true
self.displayIfNeeded() // Force display after setup
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupTrackingArea() {
// Remove existing tracking area
if let existingArea = trackingArea {
removeTrackingArea(existingArea)
}
// Create new tracking area that covers this view
trackingArea = NSTrackingArea(
rect: bounds,
options: [.activeAlways, .mouseMoved, .inVisibleRect],
owner: self,
userInfo: nil
)
if let trackingArea = trackingArea {
addTrackingArea(trackingArea)
}
}
override func updateTrackingAreas() {
super.updateTrackingAreas()
setupTrackingArea()
}
override func draw(_ dirtyRect: NSRect) {
// NSLog("🎨 DRAWING ENTRY POINT for view on screen: %@ with dirtyRect: %@", self.window?.screen?.customLocalizedName ?? "UnknownScreen", NSStringFromRect(dirtyRect))
super.draw(dirtyRect)
NSColor.black.withAlphaComponent(0.05).setFill()
dirtyRect.fill() // Lichte overlay over het hele scherm
// HERSTEL: Teken highlight voor hoveredWindow
guard let currentHoveredWindow = self.hoveredWindow,
let currentViewNSScreen = self.window?.screen, // Het NSScreen waar deze view op is
let mainScreenForConversion = self.windowCaptureManager.determinedMainScreen else {
// Als er geen window gehovered is, teken dan alleen de instructietekst op het hoofdscherm
if self.window?.screen == self.windowCaptureManager.determinedMainScreen { // Gebruik determinedMainScreen voor consistentie
drawInstructionText(on: self.window!.screen!, viewSize: self.bounds.size)
}
return
}
let scWindowFrame = currentHoveredWindow.frame
// Converteer SCWindow.frame naar Globale AppKit coördinaten (Y-omhoog, oorsprong linksonder hoofd NSScreen)
let globalHoveredWindowFrame = self.windowCaptureManager.convertSCWindowFrameToGlobal(scWindowFrame, mainScreen: mainScreenForConversion)
// Converteer de globale frame naar coördinaten lokaal aan *deze* view.
// De (0,0) van deze view is de linksonderhoek van het currentViewNSScreen.
let localHighlightX = globalHoveredWindowFrame.origin.x - currentViewNSScreen.frame.origin.x
let localHighlightY = globalHoveredWindowFrame.origin.y - currentViewNSScreen.frame.origin.y
let rectToHighlightInViewCoordinates = NSRect(x: localHighlightX,
y: localHighlightY,
width: globalHoveredWindowFrame.width,
height: globalHoveredWindowFrame.height)
// NSLog("🎨 DRAW on view for NSScreen \'%@\': Highlighting window \'%@\'", currentViewNSScreen.customLocalizedName, currentHoveredWindow.title ?? "N/A")
// NSLog("🎨 SCWindow Frame: %@ (Layer: \(currentHoveredWindow.windowLayer))", NSStringFromRect(scWindowFrame))
// NSLog("🎨 Global Hovered Frame: %@", NSStringFromRect(globalHoveredWindowFrame))
// NSLog("🎨 Calculated Highlight Rect (local to view on screen \'%@\'): %@", currentViewNSScreen.customLocalizedName, NSStringFromRect(rectToHighlightInViewCoordinates))
// Teken alleen als de highlight rect daadwerkelijk de bounds van deze view snijdt.
// Dit is belangrijk omdat een venster over meerdere schermen kan spannen.
if self.bounds.intersects(rectToHighlightInViewCoordinates) {
NSColor.blue.withAlphaComponent(0.3).setFill()
// We clippen de highlight path naar de intersectie met de view bounds voor het geval het venster groter is dan dit schermsegment.
let drawingRect = self.bounds.intersection(rectToHighlightInViewCoordinates)
let highlightPath = NSBezierPath(rect: drawingRect)
highlightPath.fill()
NSColor.blue.withAlphaComponent(0.8).setStroke()
highlightPath.lineWidth = 2 // Standaard lijndikte voor venster
highlightPath.stroke()
// NSLog("🎨 Window highlight drawn (intersected rect: %@).", NSStringFromRect(drawingRect))
} else {
// NSLog("🎨 Highlight rect %@ does NOT intersect view bounds %@. Not drawing highlight on this screen segment.", NSStringFromRect(rectToHighlightInViewCoordinates), NSStringFromRect(self.bounds))
}
// Teken instructietekst (alleen op de primary overlay)
if currentViewNSScreen == self.windowCaptureManager.determinedMainScreen { // Gebruik determinedMainScreen
drawInstructionText(on: currentViewNSScreen, viewSize: self.bounds.size)
}
}
private func drawInstructionText(on screen: NSScreen, viewSize: NSSize) {
// HERSTEL: Tekst voor window selectie
let instructionText = "Hover to select a window, Click to capture, ESC to cancel"
let attributes: [NSAttributedString.Key: Any] = [
.font: NSFont.systemFont(ofSize: 18, weight: .medium),
.foregroundColor: NSColor.white.withAlphaComponent(0.9),
.strokeColor: NSColor.black.withAlphaComponent(0.5),
.strokeWidth: -2.0,
.paragraphStyle: {
let style = NSMutableParagraphStyle()
style.alignment = .center
return style
}()
]
let attributedString = NSAttributedString(string: instructionText, attributes: attributes)
let textSize = attributedString.size()
let textRect = NSRect(x: (viewSize.width - textSize.width) / 2,
y: viewSize.height - textSize.height - 30,
width: textSize.width,
height: textSize.height)
let backgroundPadding: CGFloat = 10
let backgroundRect = NSRect(
x: textRect.origin.x - backgroundPadding,
y: textRect.origin.y - backgroundPadding,
width: textRect.width + (2 * backgroundPadding),
height: textRect.height + (2 * backgroundPadding)
)
let BORDER_RADIUS: CGFloat = 10
let textBackgroundPath = NSBezierPath(roundedRect: backgroundRect, xRadius: BORDER_RADIUS, yRadius: BORDER_RADIUS)
NSColor.black.withAlphaComponent(0.4).setFill()
textBackgroundPath.fill()
attributedString.draw(in: textRect)
// NSLog("🎨 Instruction text drawn on main screen.") // Keep this one less verbose for now
}
override func mouseMoved(with event: NSEvent) {
let globalLocation = NSEvent.mouseLocation
var currentMouseNSScreen: NSScreen?
for s in NSScreen.screens {
if s.frame.contains(globalLocation) {
currentMouseNSScreen = s
break
}
}
// Als de muis niet op het scherm van DEZE specifieke overlay view is, reset hoveredWindow voor DEZE view.
// Dit zorgt ervoor dat een venster dat over meerdere schermen spant, alleen gehighlight wordt op het scherm waar de muis is.
guard let mouseNSScreen = currentMouseNSScreen, mouseNSScreen.displayID == self.screen.displayID else {
if hoveredWindow != nil {
hoveredWindow = nil
needsDisplay = true
}
return
}
// Muis is op het scherm van deze view. Kijk welk SCWindow (indien aanwezig) gehovered wordt.
let foundWindow = windowCaptureManager.findWindowAt(location: globalLocation)
if hoveredWindow != foundWindow { // Vergelijk SCWindow objecten direct
hoveredWindow = foundWindow
needsDisplay = true
// if foundWindow != nil {
// NSLog("🖱 Hovered window SET to: \'%@\' (ID: \(foundWindow!.windowID), Layer: \(foundWindow!.windowLayer)) on screen %@", foundWindow!.title ?? "N/A", self.screen.customLocalizedName)
// } else {
// NSLog("🖱 💨 No window hovered on screen %@ at %@", self.screen.customLocalizedName, NSStringFromPoint(globalLocation))
// }
}
}
// 🔥 NIEUW: Handle mouse clicks directly in the overlay view
override func mouseDown(with event: NSEvent) {
let globalLocation = NSEvent.mouseLocation
print("🔥 OVERLAY: Mouse click detected at \(globalLocation)")
// Find window at click location
if let window = windowCaptureManager.findWindowAt(location: globalLocation) {
let selectedWindowTitle = window.title ?? "Untitled"
print("🎯 ✅ OVERLAY: Window selected: \(selectedWindowTitle) at \(globalLocation)")
// Deactivate window selection mode first
windowCaptureManager.deactivateWindowSelectionMode()
// Capture the window
Task {
print("🔥 OVERLAY: Starting window capture task for \(selectedWindowTitle)")
await windowCaptureManager.captureWindow(window)
print("🔥 OVERLAY: Window capture task completed for \(selectedWindowTitle)")
}
} else {
print("❌ OVERLAY: No window found at \(globalLocation)")
print("🔥 DEBUG: Available windows count: \(windowCaptureManager.availableWindows.count)")
// Extra debug: List all windows and their positions
for (index, win) in windowCaptureManager.availableWindows.enumerated() {
let frame = win.frame
let isPointInFrame = NSPointInRect(globalLocation, frame)
print("🔥 DEBUG: Window \(index): \(win.title ?? "Untitled") - Frame: \(frame) - Contains point: \(isPointInFrame)")
}
windowCaptureManager.deactivateWindowSelectionMode()
}
}
override func keyDown(with event: NSEvent) {
if event.keyCode == 53 { // ESC
windowCaptureManager.deactivateWindowSelectionMode()
}
}
override var acceptsFirstResponder: Bool {
return true
}
}
// MARK: - ScreenshotApp Extension for Window Capture Integration
extension ScreenshotApp {
// Add window capture manager property to ScreenshotApp
private static var windowCaptureManagerKey: UInt8 = 0
var windowCaptureManager: WindowCaptureManager? {
get {
return objc_getAssociatedObject(self, &ScreenshotApp.windowCaptureManagerKey) as? WindowCaptureManager
}
set {
objc_setAssociatedObject(self, &ScreenshotApp.windowCaptureManagerKey, newValue, .OBJC_ASSOCIATION_RETAIN)
}
}
func initializeWindowCaptureManager() {
if #available(macOS 12.3, *) {
windowCaptureManager = WindowCaptureManager(screenshotApp: self)
print("✅ WindowCaptureManager initialized")
} else {
print("⚠️ WindowCaptureManager requires macOS 12.3 or later")
}
}
func processWindowCapture(image: NSImage, windowTitle: String?) {
// Process window capture similar to regular screenshot capture
print("🪟 Processing window capture: \(windowTitle ?? "Untitled Window")")
let tempURL = createTempUrl()
setTempFileURL(tempURL)
guard let tiffData = image.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: tiffData),
let pngData = bitmap.representation(using: .png, properties: [:]) else {
print("❌ Failed to convert window capture to PNG")
return
}
do {
try pngData.write(to: tempURL)
print("💾 Window capture saved temporarily: \(tempURL.path)")
// Play sound if enabled
if SettingsManager.shared.playSoundOnCapture {
NSSound(named: "Glass")?.play()
}
// Show preview with window title context
showPreview(image: image, windowTitle: windowTitle)
} catch {
print("❌ Failed to save window capture: \(error)")
}
}
private func showPreview(image: NSImage, windowTitle: String?) {
// Use existing preview logic but potentially add window title to UI
showPreview(image: image)
// You could extend the preview UI to show window title
if let title = windowTitle {
print("📸 Window captured: \(title)")
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.screencapture</key>
<true/>
</dict>
</plist>
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>ShotScreen Updates</title>
<link>https://git.plet.i234.me/Nick/shotscreen/raw/branch/main/appcast.xml</link>
<description>ShotScreen Updates</description>
<language>en</language>
<item>
<title>ShotScreen 1.0</title>
<description><![CDATA[
<h2>🎉 ShotScreen 1.0 - Initial Release</h2>
<p>The first official release of ShotScreen with full screenshot capabilities!</p>
<ul>
<li>📸 Advanced screenshot capture modes</li>
<li>🖱️ Intuitive user interface</li>
<li>🔄 Automatic update system</li>
<li>💾 Smart file management</li>
<li>🎯 Multi-monitor support</li>
</ul>
]]></description>
<pubDate>Fri, 28 Jun 2024 14:00:00 +0000</pubDate>
<enclosure url="https://git.plet.i234.me/Nick/shotscreen/releases/download/v1.0/ShotScreen-1.0.dmg"
sparkle:version="1.0"
sparkle:shortVersionString="1.0"
length="11218106"
type="application/octet-stream" />
<sparkle:minimumSystemVersion>13.0</sparkle:minimumSystemVersion>
</item>
</channel>
</rss>
+18
View File
@@ -0,0 +1,18 @@
{
"fileFormatVersion": "1.0.0",
"itemInfoEntries": {
"216F8D6B-C0C1-4AE6-BEF9-618A1A41B9DD": {
"author": "com.apple.CoreML",
"description": "CoreML Model Weights",
"name": "weights",
"path": "com.apple.CoreML/weights"
},
"B321596D-B0BF-46B8-8D28-69DBA4172E49": {
"author": "com.apple.CoreML",
"description": "CoreML Model Specification",
"name": "model.mlmodel",
"path": "com.apple.CoreML/model.mlmodel"
}
},
"rootModelIdentifier": "B321596D-B0BF-46B8-8D28-69DBA4172E49"
}
+373
View File
@@ -0,0 +1,373 @@
#!/bin/bash
# ShotScreen - Professional Build & Sign Script
# This script creates a properly signed and notarized build to prevent antivirus false positives
set -e # Exit on any error
echo "🔨 Starting professional build process for ShotScreen..."
# Configuration
APP_NAME="ShotScreen"
BUNDLE_ID="com.shotscreen.app"
DEVELOPER_ID="Developer ID Application: Nick Roodenrijs (HX9B36NN8V)"
KEYCHAIN_PROFILE="ShotScreen-Profile" # Create this with 'xcrun notarytool store-credentials'
# Build paths - FIXED: Use SwiftPM standard paths like build_app.sh
DIST_DIR="./dist"
APP_DIR="$DIST_DIR/$APP_NAME.app"
CONTENTS_DIR="$APP_DIR/Contents"
MACOS_DIR="$CONTENTS_DIR/MacOS"
RESOURCES_DIR="$CONTENTS_DIR/Resources"
FRAMEWORKS_DIR="$CONTENTS_DIR/Frameworks"
# Create dist directory
rm -rf "$DIST_DIR"
mkdir -p "$DIST_DIR"
# Read version from Info.plist for DMG naming
if [ -f "Info.plist" ]; then
APP_VERSION=$(plutil -extract CFBundleShortVersionString raw Info.plist 2>/dev/null || echo "1.0")
else
APP_VERSION="1.4"
fi
DMG_PATH="./dist/$APP_NAME-$APP_VERSION.dmg"
echo "📦 Building release version..."
# FIXED: Use standard SwiftPM release build like build_app.sh does for debug
swift build --configuration release
# FIXED: Get the path to the SwiftPM built binary (like build_app.sh)
BINARY_PATH=".build/release/ShotScreen"
# Verify binary exists
if [ ! -f "$BINARY_PATH" ]; then
echo "❌ Error: Built binary not found at $BINARY_PATH"
echo "Available files in .build/release/:"
ls -la .build/release/ || echo "No .build/release/ directory found"
exit 1
fi
echo "✅ Found built binary at: $BINARY_PATH"
echo "🏗️ Creating app bundle..."
# Create app bundle structure (like build_app.sh)
mkdir -p "$MACOS_DIR"
mkdir -p "$RESOURCES_DIR"
mkdir -p "$FRAMEWORKS_DIR"
# Copy binary to app bundle and rename it to ShotScreen
echo "Copying binary to app bundle and renaming to ShotScreen..."
cp "$BINARY_PATH" "$MACOS_DIR/ShotScreen"
# Verify the copy was successful
if [ -f "$MACOS_DIR/ShotScreen" ]; then
echo "✅ Binary successfully copied to app bundle"
ls -la "$MACOS_DIR/ShotScreen"
else
echo "❌ Error: Failed to copy binary to app bundle"
exit 1
fi
# Copy Info.plist
echo "Copying Info.plist..."
cp Info.plist "$CONTENTS_DIR/"
# Copy app icon
echo "Copying app icon..."
if [ -f "./AppIcon.icns" ]; then
cp "./AppIcon.icns" "$RESOURCES_DIR/"
echo "✅ Copied AppIcon.icns to app bundle Resources"
else
echo "⚠️ Warning: AppIcon.icns not found"
fi
# Copy resource images
if [ -d "ShotScreen/Sources/images" ]; then
cp -r "ShotScreen/Sources/images" "$RESOURCES_DIR/" 2>/dev/null || true
echo "✅ Copied resource images"
fi
# Copy frameworks (using same logic as build_app.sh)
echo "📦 Copying frameworks..."
# Find and copy Sparkle framework
SPARKLE_FRAMEWORK_PATH=""
for framework_path in .build/release/*.framework .build/*/release/*.framework .build/release/PackageFrameworks/*.framework; do
if [[ -d "$framework_path" && "$(basename "$framework_path")" == "Sparkle.framework" ]]; then
SPARKLE_FRAMEWORK_PATH="$framework_path"
break
fi
done
if [[ -n "$SPARKLE_FRAMEWORK_PATH" && -d "$SPARKLE_FRAMEWORK_PATH" ]]; then
echo "✅ Found Sparkle framework at: $SPARKLE_FRAMEWORK_PATH"
cp -R "$SPARKLE_FRAMEWORK_PATH" "$FRAMEWORKS_DIR/"
echo "✅ Copied Sparkle framework to app bundle"
else
echo "❌ Warning: Sparkle framework not found in SwiftPM build output"
echo "🔍 Searching in .build directory:"
find .build -name "*.framework" -type d 2>/dev/null || echo "No frameworks found"
# Try alternative locations for Sparkle
if [ -d ".build/arm64-apple-macosx/release/Sparkle.framework" ]; then
cp -R ".build/arm64-apple-macosx/release/Sparkle.framework" "$FRAMEWORKS_DIR/"
echo "✅ Copied Sparkle framework from arm64 location"
else
echo "❌ Sparkle framework not found!"
exit 1
fi
fi
# Configure @rpath for framework loading (like build_app.sh)
echo "🔧 Configuring @rpath for framework loading..."
install_name_tool -add_rpath "@loader_path/../Frameworks" "$MACOS_DIR/ShotScreen"
if [ $? -eq 0 ]; then
echo "✅ Added @rpath pointing to ../Frameworks directory"
else
echo "❌ Failed to add @rpath configuration"
fi
# Create PkgInfo file
echo "APPLSCR" > "$CONTENTS_DIR/PkgInfo"
# Make the app bundle executable
chmod +x "$MACOS_DIR/ShotScreen"
echo "✍️ Code signing with hardened runtime..."
# Sign frameworks first
echo "🔐 Signing frameworks..."
for framework in "$FRAMEWORKS_DIR"/*.framework; do
if [ -d "$framework" ]; then
echo " Signing framework: $(basename "$framework")"
codesign --sign "$DEVELOPER_ID" \
--force \
--timestamp \
--options runtime \
"$framework"
echo " ✅ Signed framework: $(basename "$framework")"
fi
done
# Sign with hardened runtime to prevent malware-like behavior detection
echo "🔐 Signing main app bundle..."
codesign --sign "$DEVELOPER_ID" \
--deep \
--force \
--options runtime \
--entitlements ShotScreen/entitlements.plist \
--timestamp \
"$APP_DIR"
echo "🔍 Verifying signature..."
codesign --verify --deep --strict "$APP_DIR"
echo "✅ Code signature is valid"
# Note: spctl assessment may fail for unnotarized apps, but that's okay for local testing
if spctl --assess --type exec "$APP_DIR" 2>/dev/null; then
echo "✅ Gatekeeper assessment passed"
else
echo "⚠️ Gatekeeper assessment failed (app needs notarization for distribution)"
fi
echo "📦 Creating professional DMG with custom background and layout..."
# FIXED: Use unique volume name to avoid conflicts with existing ShotScreen volume
DMG_VOLUME_NAME="$APP_NAME-$APP_VERSION"
# First unmount any existing volumes to prevent conflicts
hdiutil detach "/Volumes/$DMG_VOLUME_NAME" 2>/dev/null || true
# Create a temporary folder for DMG contents
TEMP_DMG_DIR="./temp_dmg"
rm -rf "$TEMP_DMG_DIR"
mkdir -p "$TEMP_DMG_DIR"
# Copy app to temp folder
cp -R "$APP_DIR" "$TEMP_DMG_DIR/"
# Create symlink to Applications folder
echo "🔗 Creating Applications symlink..."
ln -s /Applications "$TEMP_DMG_DIR/Applications"
# Copy background image to temp folder
echo "🎨 Adding custom background image..."
if [ -f "./ShotScreen/Sources/images/BannerFinder.png" ]; then
mkdir -p "$TEMP_DMG_DIR/.background"
cp "./ShotScreen/Sources/images/BannerFinder.png" "$TEMP_DMG_DIR/.background/background.png"
echo "✅ Background image added (BannerFinder.png)"
else
echo "⚠️ Warning: BannerFinder.png not found in ShotScreen/Sources/images/"
fi
# Create initial DMG (read-write for customization)
TEMP_DMG_PATH="./temp_dmg_rw.dmg"
rm -f "$TEMP_DMG_PATH"
hdiutil create -volname "$DMG_VOLUME_NAME" \
-srcfolder "$TEMP_DMG_DIR" \
-ov \
-format UDRW \
-size 100m \
"$TEMP_DMG_PATH"
if [ $? -ne 0 ]; then
echo "❌ DMG creation failed!"
echo "🔍 Checking if volume is still mounted..."
mount | grep -i shotscreen || echo "No ShotScreen volumes mounted"
# Clean up temp folder
rm -rf "$TEMP_DMG_DIR"
exit 1
fi
# Mount the DMG for customization
echo "🔧 Mounting DMG for customization..."
hdiutil attach "$TEMP_DMG_PATH" -mountpoint "/Volumes/$DMG_VOLUME_NAME" -nobrowse
# Wait for mount to complete
sleep 2
# Customize DMG appearance with AppleScript
echo "✨ Customizing DMG appearance with background and layout..."
cat > dmg_style.applescript << 'EOF'
tell application "Finder"
tell disk "DMG_VOLUME_NAME_PLACEHOLDER"
open
-- Wait for window to open
delay 3
set current view of container window to icon view
set toolbar visible of container window to false
set statusbar visible of container window to false
-- Smaller window size (500x350 instead of 500x300)
set the bounds of container window to {300, 100, 800, 450}
set theViewOptions to the icon view options of container window
set arrangement of theViewOptions to not arranged
set icon size of theViewOptions to 96 -- Large icons
set text size of theViewOptions to 12
-- Set background picture
set background picture of theViewOptions to file ".background:background.png"
-- Wait for settings to apply
delay 2
-- Position icons with more space
set position of item "ShotScreen.app" of container window to {150, 200}
set position of item "Applications" of container window to {350, 200}
-- Force update and wait
update without registering applications
delay 3
-- Close and reopen to ensure settings stick
close
delay 1
open
delay 2
end tell
end tell
EOF
# Replace placeholder with actual volume name
sed -i '' "s/DMG_VOLUME_NAME_PLACEHOLDER/$DMG_VOLUME_NAME/g" dmg_style.applescript
# Apply the styling
echo "🎨 Applying AppleScript styling..."
if osascript dmg_style.applescript; then
echo "✅ AppleScript styling applied successfully"
else
echo "⚠️ AppleScript styling failed, trying alternative method..."
# Alternative: Set view manually via Finder automation
cat > dmg_style_alt.applescript << 'EOF'
tell application "Finder"
tell disk "DMG_VOLUME_NAME_PLACEHOLDER"
open
delay 5
-- Simple settings that usually work
set current view of container window to icon view
delay 1
set theViewOptions to the icon view options of container window
set icon size of theViewOptions to 96
delay 1
close
delay 1
open
delay 2
end tell
end tell
EOF
# Replace placeholder and run alternative
sed -i '' "s/DMG_VOLUME_NAME_PLACEHOLDER/$DMG_VOLUME_NAME/g" dmg_style_alt.applescript
osascript dmg_style_alt.applescript 2>/dev/null || echo "⚠️ Alternative styling also failed"
rm -f dmg_style_alt.applescript
fi
# Clean up AppleScript
rm -f dmg_style.applescript
# Force create .DS_Store with proper settings
echo "📁 Setting Finder view options..."
if [ -d "/Volumes/$DMG_VOLUME_NAME" ]; then
# Ensure background image is accessible
if [ -f "/Volumes/$DMG_VOLUME_NAME/.background/background.png" ]; then
echo "✅ Background image found in DMG"
else
echo "⚠️ Background image not found in DMG"
fi
# Force Finder to acknowledge the DMG
osascript -e "tell application \"Finder\" to open disk \"$DMG_VOLUME_NAME\"" 2>/dev/null || true
sleep 3
# Let Finder settle and write .DS_Store
sleep 5
fi
# Unmount the DMG
echo "📤 Unmounting DMG..."
hdiutil detach "/Volumes/$DMG_VOLUME_NAME"
# Convert to compressed read-only DMG
echo "🗜️ Converting to final compressed DMG..."
hdiutil convert "$TEMP_DMG_PATH" \
-format UDZO \
-o "$DMG_PATH"
# Clean up temporary files
rm -f "$TEMP_DMG_PATH"
rm -rf "$TEMP_DMG_DIR"
echo "✅ Professional DMG created successfully with custom background and layout!"
echo "✍️ Signing DMG..."
codesign --sign "$DEVELOPER_ID" \
--timestamp \
"$DMG_PATH"
echo "📤 Submitting for notarization..."
if xcrun notarytool submit "$DMG_PATH" --keychain-profile "$KEYCHAIN_PROFILE" --wait; then
echo "✅ Notarization successful!"
echo "🔍 Stapling notarization ticket to DMG..."
xcrun stapler staple "$DMG_PATH"
echo "✅ DMG is now notarized and ready for distribution!"
else
echo "⚠️ Notarization failed or timed out"
echo "💡 You can check status with: xcrun notarytool history --keychain-profile $KEYCHAIN_PROFILE"
echo "💡 Manual stapling: xcrun stapler staple $DMG_PATH"
fi
echo ""
echo "✅ Build complete!"
echo "📦 DMG created at: $DMG_PATH"
echo "✅ Code signed with Developer ID: Nick Roodenrijs (HX9B36NN8V)"
echo "🎯 DMG ready for hufterproof release script!"
BIN
View File
Binary file not shown.
+320
View File
@@ -0,0 +1,320 @@
#!/bin/bash
# 🚀 SUPER HUFTERPROOF ShotScreen Release Script V2
# Zero HTML, Zero Problems, 100% Bulletproof
# 🎯 NEW: Auto-increment version detection!
#
# Usage:
# ./release_hufterproof_v2.sh # Auto-increment from current version
# ./release_hufterproof_v2.sh 1.15 # Use specific version
set -e
# Colors
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
PURPLE='\033[0;35m'
NC='\033[0m'
# Config
GITEA_URL="https://git.plet.i234.me"
RELEASES_REPO="Nick/shotscreen"
echo -e "${PURPLE}🚀 SUPER HUFTERPROOF Release Script V2${NC}"
echo -e "${PURPLE}=====================================${NC}"
# Get current version from Info.plist and auto-increment
get_next_version() {
if [ -f "Info.plist" ]; then
CURRENT_VERSION=$(plutil -extract CFBundleShortVersionString raw Info.plist 2>/dev/null || echo "1.0")
echo -e "${BLUE}🔍 Current version: $CURRENT_VERSION${NC}"
# Split version into parts (e.g., "1.12" -> major=1, minor=12)
IFS='.' read -r MAJOR MINOR <<< "$CURRENT_VERSION"
# Increment minor version
NEXT_MINOR=$((MINOR + 1))
SUGGESTED_VERSION="$MAJOR.$NEXT_MINOR"
echo -e "${GREEN}🚀 Suggested next version: $SUGGESTED_VERSION${NC}"
return 0
else
SUGGESTED_VERSION="1.0"
echo -e "${YELLOW}⚠️ No Info.plist found, suggesting: $SUGGESTED_VERSION${NC}"
return 1
fi
}
# Determine version
NEW_VERSION="$1"
if [ -z "$NEW_VERSION" ]; then
get_next_version
echo -e "${YELLOW}Press ENTER to use suggested version ($SUGGESTED_VERSION) or type custom version:${NC}"
read -r USER_INPUT
if [ -z "$USER_INPUT" ]; then
NEW_VERSION="$SUGGESTED_VERSION"
echo -e "${GREEN}✅ Using suggested version: $NEW_VERSION${NC}"
else
NEW_VERSION="$USER_INPUT"
echo -e "${BLUE}✅ Using custom version: $NEW_VERSION${NC}"
fi
fi
echo -e "${BLUE}📋 Building version: $NEW_VERSION${NC}"
# Update Info.plist
echo -e "${BLUE}📝 Updating Info.plist...${NC}"
plutil -replace CFBundleVersion -string "$NEW_VERSION" Info.plist
plutil -replace CFBundleShortVersionString -string "$NEW_VERSION" Info.plist
# Update build_release_signed.sh with new version
echo -e "${BLUE}📝 Updating build script version...${NC}"
sed -i '' "s/APP_VERSION=\".*\"/APP_VERSION=\"$NEW_VERSION\"/" build_release_signed.sh
# Build app with Developer ID signing and notarization
echo -e "${BLUE}🔨 Building app with Developer ID signing...${NC}"
if ! ./build_release_signed.sh; then
echo -e "${RED}❌ Build failed${NC}"
exit 1
fi
# Check DMG exists
DMG_PATH="./dist/ShotScreen-$NEW_VERSION.dmg"
if [ ! -f "$DMG_PATH" ]; then
echo -e "${RED}❌ DMG not found: $DMG_PATH${NC}"
exit 1
fi
# Get signature and size
echo -e "${BLUE}🔐 Getting signature...${NC}"
SIG_OUTPUT=$(./.build/artifacts/sparkle/Sparkle/bin/sign_update "$DMG_PATH")
SIGNATURE=$(echo "$SIG_OUTPUT" | grep -o 'sparkle:edSignature="[^"]*"' | cut -d'"' -f2)
DMG_SIZE=$(stat -f%z "$DMG_PATH")
if [ -z "$SIGNATURE" ]; then
echo -e "${RED}❌ Signature generation failed${NC}"
exit 1
fi
echo -e "${GREEN}✅ Signature: ${SIGNATURE:0:20}...${NC}"
echo -e "${GREEN}✅ Size: $DMG_SIZE bytes${NC}"
# Get release notes from TXT file (preserve original formatting)
RELEASE_NOTES=$(awk '
/^ShotScreen [0-9]/ {
if (count++ > 0) exit
next
}
/^=+$/ { next }
/^[A-Za-z]/ && !/^ShotScreen/ && NF > 0 {
print $0
}
/^ / && NF > 0 {
print $0
}
' release_notes.txt)
if [ -z "$RELEASE_NOTES" ]; then
RELEASE_NOTES="Release version $NEW_VERSION"
fi
# Create Gitea release
echo -e "${BLUE}📦 Creating Gitea release...${NC}"
# Format release notes beautifully for Gitea (markdown format)
FORMATTED_NOTES="## 🚀 ShotScreen $NEW_VERSION Features
$RELEASE_NOTES"
# Clean JSON (proper escaping for multiline text)
CLEAN_NOTES=$(printf "%s" "$FORMATTED_NOTES" | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}' | sed 's/\\n$//')
JSON_PAYLOAD="{
\"tag_name\": \"v$NEW_VERSION\",
\"name\": \"ShotScreen v$NEW_VERSION\",
\"body\": \"$CLEAN_NOTES\",
\"draft\": false,
\"prerelease\": false
}"
RESPONSE=$(curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "$JSON_PAYLOAD" \
"$GITEA_URL/api/v1/repos/$RELEASES_REPO/releases")
RELEASE_ID=$(echo "$RESPONSE" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
if [ -z "$RELEASE_ID" ]; then
echo -e "${YELLOW}⚠️ Checking for existing release...${NC}"
EXISTING=$(curl -s "$GITEA_URL/api/v1/repos/$RELEASES_REPO/releases/tags/v$NEW_VERSION")
RELEASE_ID=$(echo "$EXISTING" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
fi
if [ -z "$RELEASE_ID" ]; then
echo -e "${RED}❌ Could not create/find release${NC}"
exit 1
fi
echo -e "${GREEN}✅ Release ID: $RELEASE_ID${NC}"
# Upload DMG
echo -e "${BLUE}📤 Uploading DMG...${NC}"
UPLOAD_RESPONSE=$(curl -s -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$DMG_PATH" \
"$GITEA_URL/api/v1/repos/$RELEASES_REPO/releases/$RELEASE_ID/assets")
ASSET_ID=$(echo "$UPLOAD_RESPONSE" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
if [ -z "$ASSET_ID" ]; then
echo -e "${RED}❌ DMG upload failed${NC}"
exit 1
fi
echo -e "${GREEN}✅ DMG uploaded${NC}"
# Create super simple appcast (no HTML, just plain text)
echo -e "${BLUE}📄 Creating simple appcast...${NC}"
# Convert plain text to simple HTML for appcast
HTML_NOTES=$(echo "$RELEASE_NOTES" | sed 's/^- /<li>/' | sed 's/$/<\/li>/')
if [[ "$HTML_NOTES" == *"<li>"* ]]; then
HTML_NOTES="<ul>$HTML_NOTES</ul>"
fi
# Clean up any old appcast first
rm -f appcast.xml
echo -e "${BLUE}📄 Generating appcast for version $NEW_VERSION...${NC}"
cat > appcast.xml << EOF
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>ShotScreen Updates</title>
<link>$GITEA_URL/$RELEASES_REPO/raw/branch/main/appcast.xml</link>
<description>ShotScreen Updates</description>
<language>en</language>
<item>
<title>ShotScreen $NEW_VERSION</title>
<description><![CDATA[
<h2>ShotScreen $NEW_VERSION</h2>
$HTML_NOTES
]]></description>
<pubDate>$(date -R)</pubDate>
<enclosure url="$GITEA_URL/$RELEASES_REPO/releases/download/v$NEW_VERSION/ShotScreen-$NEW_VERSION.dmg"
sparkle:version="$NEW_VERSION"
sparkle:shortVersionString="$NEW_VERSION"
length="$DMG_SIZE"
type="application/octet-stream"
sparkle:edSignature="$SIGNATURE" />
<sparkle:minimumSystemVersion>13.0</sparkle:minimumSystemVersion>
</item>
</channel>
</rss>
EOF
# Validate appcast
if ! xmllint --noout appcast.xml 2>/dev/null; then
echo -e "${RED}❌ Appcast validation failed${NC}"
exit 1
fi
echo -e "${GREEN}✅ Valid appcast created${NC}"
# Debug: Show what we're about to deploy
echo -e "${BLUE}🔍 DEBUG: Appcast content preview:${NC}"
echo "Version: $(grep 'sparkle:version=' appcast.xml | head -1)"
echo "URL: $(grep 'enclosure url=' appcast.xml | head -1 | cut -d'"' -f2)"
# Deploy appcast
echo -e "${BLUE}🚀 Deploying appcast...${NC}"
RELEASES_DIR="../shotscreen"
# Clean up any existing problematic repository
if [ -d "$RELEASES_DIR" ]; then
echo -e "${YELLOW}🧹 Cleaning up existing repository...${NC}"
rm -rf "$RELEASES_DIR"
fi
# Create fresh directory and clone
mkdir -p "$RELEASES_DIR"
cd "$RELEASES_DIR"
echo -e "${BLUE}📥 Fresh clone from remote...${NC}"
git clone "$GITEA_URL/$RELEASES_REPO.git" . || {
echo -e "${YELLOW}⚠️ Remote repository doesn't exist or clone failed, creating new repo...${NC}"
git init
git remote add origin "$GITEA_URL/$RELEASES_REPO.git"
}
# Configure git for this repository
git config pull.rebase false # Use merge instead of rebase
git config user.name "ShotScreen Release Bot" || true
git config user.email "releases@shotscreen.app" || true
# Backup old appcast for comparison
if [ -f "appcast.xml" ]; then
echo -e "${BLUE}📋 Backing up old appcast...${NC}"
cp "appcast.xml" "appcast.xml.backup"
echo "Old version: $(grep 'sparkle:version=' "appcast.xml" | head -1)"
fi
# Copy new appcast
cd - > /dev/null
cp appcast.xml "$RELEASES_DIR/"
cd "$RELEASES_DIR"
# Add and commit
git add appcast.xml
git commit -m "Deploy appcast for v$NEW_VERSION" || true
# Push with force if needed (since we're authoritative for appcast)
echo -e "${BLUE}📤 Pushing appcast to remote...${NC}"
if ! git push origin main; then
echo -e "${YELLOW}⚠️ Normal push failed, force pushing appcast update...${NC}"
git push origin main --force
fi
cd - > /dev/null
# Test everything
echo -e "${BLUE}🧪 Testing...${NC}"
sleep 2
# Test appcast download
if ! curl -s "$GITEA_URL/$RELEASES_REPO/raw/branch/main/appcast.xml" | grep -q "$NEW_VERSION"; then
echo -e "${RED}❌ Appcast test failed${NC}"
exit 1
fi
# Test DMG download
if ! curl -I -s "$GITEA_URL/$RELEASES_REPO/releases/download/v$NEW_VERSION/ShotScreen-$NEW_VERSION.dmg" | grep -q "200"; then
echo -e "${RED}❌ DMG test failed${NC}"
exit 1
fi
# Git operations
echo -e "${BLUE}📝 Git operations...${NC}"
git add .
git commit -m "Release v$NEW_VERSION" || true
git tag -a "v$NEW_VERSION" -m "Release v$NEW_VERSION" || true
git push -u origin main || git push origin main --force
git push origin "v$NEW_VERSION" || true
echo
echo -e "${GREEN}🎉 SUPER HUFTERPROOF RELEASE COMPLETE! 🎉${NC}"
echo -e "${GREEN}======================================${NC}"
echo -e "${GREEN}✅ Version: $NEW_VERSION${NC}"
echo -e "${GREEN}✅ DMG Size: $DMG_SIZE bytes${NC}"
echo -e "${GREEN}✅ All tests: PASSED${NC}"
echo
echo -e "${BLUE}🌐 Release: $GITEA_URL/$RELEASES_REPO/releases/tag/v$NEW_VERSION${NC}"
echo -e "${BLUE}📡 Appcast: $GITEA_URL/$RELEASES_REPO/raw/branch/main/appcast.xml${NC}"
echo
echo -e "${YELLOW}🎯 Ready to test updates on other computer!${NC}"