├── .cursorignore ├── ScreenshotOrganizer ├── Assets.xcassets │ ├── Contents.json │ ├── icon.imageset │ │ ├── icon-512.png │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── icon-1024.png │ │ ├── icon-128.png │ │ ├── icon-16.png │ │ ├── icon-256.png │ │ ├── icon-32.png │ │ ├── icon-512.png │ │ ├── icon-64.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── main.swift ├── KeyboardShortcutNames.swift ├── Constants.swift ├── ScreenshotOrganizer.entitlements ├── Info.plist ├── AppLogger.swift ├── SettingsContentView.swift ├── FileMonitor.swift └── AppDelegate.swift ├── .vscode ├── settings.json └── launch.json ├── ScreenshotOrganizer.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── reorx.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── swiftpm │ │ └── Package.resolved ├── xcuserdata │ └── reorx.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── project.pbxproj ├── .gitignore ├── Package.resolved ├── ScreenshotOrganizerLauncher ├── ScreenshotOrganizerLauncher.entitlements ├── main.swift └── Info.plist ├── Package.swift ├── .cursorrules ├── project.yml ├── dev_notes.md ├── docs └── github-actions.md ├── README.md └── .github └── workflows └── build.yml /.cursorignore: -------------------------------------------------------------------------------- 1 | *.xcodeproj 2 | -------------------------------------------------------------------------------- /ScreenshotOrganizer/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ScreenshotOrganizer/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ScreenshotOrganizer/main.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | let app = NSApplication.shared 4 | let delegate = AppDelegate() 5 | app.delegate = delegate 6 | app.run() 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "lldb.library": "/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Versions/A/LLDB", 3 | "lldb.launch.expressions": "native" 4 | } 5 | -------------------------------------------------------------------------------- /ScreenshotOrganizer/Assets.xcassets/icon.imageset/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reorx/screenshot-organizer/HEAD/ScreenshotOrganizer/Assets.xcassets/icon.imageset/icon-512.png -------------------------------------------------------------------------------- /ScreenshotOrganizer/Assets.xcassets/AppIcon.appiconset/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reorx/screenshot-organizer/HEAD/ScreenshotOrganizer/Assets.xcassets/AppIcon.appiconset/icon-1024.png -------------------------------------------------------------------------------- /ScreenshotOrganizer/Assets.xcassets/AppIcon.appiconset/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reorx/screenshot-organizer/HEAD/ScreenshotOrganizer/Assets.xcassets/AppIcon.appiconset/icon-128.png -------------------------------------------------------------------------------- /ScreenshotOrganizer/Assets.xcassets/AppIcon.appiconset/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reorx/screenshot-organizer/HEAD/ScreenshotOrganizer/Assets.xcassets/AppIcon.appiconset/icon-16.png -------------------------------------------------------------------------------- /ScreenshotOrganizer/Assets.xcassets/AppIcon.appiconset/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reorx/screenshot-organizer/HEAD/ScreenshotOrganizer/Assets.xcassets/AppIcon.appiconset/icon-256.png -------------------------------------------------------------------------------- /ScreenshotOrganizer/Assets.xcassets/AppIcon.appiconset/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reorx/screenshot-organizer/HEAD/ScreenshotOrganizer/Assets.xcassets/AppIcon.appiconset/icon-32.png -------------------------------------------------------------------------------- /ScreenshotOrganizer/Assets.xcassets/AppIcon.appiconset/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reorx/screenshot-organizer/HEAD/ScreenshotOrganizer/Assets.xcassets/AppIcon.appiconset/icon-512.png -------------------------------------------------------------------------------- /ScreenshotOrganizer/Assets.xcassets/AppIcon.appiconset/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reorx/screenshot-organizer/HEAD/ScreenshotOrganizer/Assets.xcassets/AppIcon.appiconset/icon-64.png -------------------------------------------------------------------------------- /ScreenshotOrganizer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ScreenshotOrganizer.xcodeproj/project.xcworkspace/xcuserdata/reorx.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reorx/screenshot-organizer/HEAD/ScreenshotOrganizer.xcodeproj/project.xcworkspace/xcuserdata/reorx.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /ScreenshotOrganizer/KeyboardShortcutNames.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import KeyboardShortcuts 3 | 4 | extension KeyboardShortcuts.Name { 5 | static let openScreenshotsFolder = Self("openScreenshotsFolder") 6 | static let openScreenRecordingsFolder = Self("openScreenRecordingsFolder") 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build artifacts 2 | .build/ 3 | *.xcworkspace 4 | xcuserdata/ 5 | DerivedData/ 6 | 7 | # macOS 8 | .DS_Store 9 | 10 | # Xcode 11 | *.xcuserstate 12 | *.xccheckout 13 | *.xcscmblueprint 14 | 15 | # Swift Package Manager 16 | .swiftpm/ 17 | Package.resolved 18 | 19 | # CocoaPods 20 | Pods/ 21 | Podfile.lock 22 | 23 | # Carthage 24 | Carthage/Build 25 | 26 | # Logs 27 | *.log -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "KeyboardShortcuts", 6 | "repositoryURL": "https://github.com/sindresorhus/KeyboardShortcuts", 7 | "state": { 8 | "branch": null, 9 | "revision": "ac12762853126cf2e7ad63a6a58e1c9f58c6a0ee", 10 | "version": "1.17.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /ScreenshotOrganizer/Assets.xcassets/icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon-512.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "icon-512.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "icon-512.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ScreenshotOrganizer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "69c39523ac0471922b625cc56b8722155cfd2afd660bdd4f5ff67335b5b87714", 3 | "pins" : [ 4 | { 5 | "identity" : "keyboardshortcuts", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/sindresorhus/KeyboardShortcuts", 8 | "state" : { 9 | "revision" : "ac12762853126cf2e7ad63a6a58e1c9f58c6a0ee", 10 | "version" : "1.17.0" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /ScreenshotOrganizer/Constants.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | enum SettingsKey { 4 | static let monitoredDirectory = "monitoredDirectory" 5 | static let logDirectory = "logDirectory" 6 | static let launchAtLogin = "launchAtLogin" 7 | } 8 | 9 | struct SettingsDefault { 10 | static let monitoredDirectory = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!.path 11 | static let logDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path 12 | static let launchAtLogin = false 13 | } 14 | -------------------------------------------------------------------------------- /ScreenshotOrganizerLauncher/ScreenshotOrganizerLauncher.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | com.reorx.ScreenshotOrganizer 10 | 11 | com.apple.security.automation.apple-events 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ScreenshotOrganizer.xcodeproj/xcuserdata/reorx.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | ScreenshotOrganizer.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 2 11 | 12 | ScreenshotOrganizerLauncher.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 1 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /ScreenshotOrganizerLauncher/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import AppKit 3 | 4 | let mainAppIdentifier = "com.reorx.ScreenshotOrganizer" 5 | let mainAppPath = Bundle.main.bundlePath.replacingOccurrences(of: "/Contents/Library/LoginItems/ScreenshotOrganizerLauncher.app", with: "") 6 | 7 | let runningApps = NSWorkspace.shared.runningApplications 8 | let isRunning = runningApps.contains { $0.bundleIdentifier == mainAppIdentifier } 9 | print("isRunning: \(isRunning)") 10 | 11 | if !isRunning { 12 | print("Not running, opening main app") 13 | let url = URL(fileURLWithPath: mainAppPath) 14 | let configuration = NSWorkspace.OpenConfiguration() 15 | NSWorkspace.shared.openApplication(at: url, configuration: configuration) { _, _ in } 16 | } 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "screenshot-organizer", 8 | platforms: [ 9 | .macOS("12.0") 10 | ], 11 | products: [ 12 | .executable(name: "ScreenshotOrganizer", targets: ["ScreenshotOrganizer"]) 13 | ], 14 | dependencies: [ 15 | .package(url: "https://github.com/sindresorhus/KeyboardShortcuts", from: "1.0.0") 16 | ], 17 | targets: [ 18 | .executableTarget( 19 | name: "ScreenshotOrganizer", 20 | dependencies: ["KeyboardShortcuts"], 21 | path: "ScreenshotOrganizer" 22 | ) 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /ScreenshotOrganizer/ScreenshotOrganizer.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.application-groups 8 | 9 | com.reorx.ScreenshotOrganizer 10 | 11 | com.apple.security.files.desktop.read-write 12 | 13 | com.apple.security.files.downloads.read-write 14 | 15 | com.apple.security.files.pictures.read-write 16 | 17 | com.apple.security.files.user-selected.read-write 18 | 19 | com.apple.security.temporary-exception.apple-events 20 | 21 | com.reorx.ScreenshotOrganizer.Launcher 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ScreenshotOrganizer/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "0.584", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "0.694", 28 | "red" : "0.325" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- 1 | You are an expert AI programming assistant that primarily focuses on producing clear, readable SwiftUI code. 2 | 3 | You always use the latest version of SwiftUI and Swift, and you are familiar with the latest features and best practices. 4 | 5 | You carefully provide accurate, factual, thoughtful answers, and excel at reasoning. 6 | 7 | - Use project.yml and xcodegen to maintain the xcode project 8 | - Follow the user's requirements carefully & to the letter. 9 | - First think step-by-step - describe your plan for what to build in pseudocode, written out in great detail. 10 | - Always write correct, up to date, bug free, fully functional and working, secure, performant and efficient code. 11 | - Focus on readability over being performant. 12 | - Fully implement all requested functionality. 13 | - Be concise. Minimize any other prose. 14 | - If you think there might not be a correct answer, you say so. If you do not know the answer, say so instead of guessing. 15 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "sweetpad-lldb", 6 | "request": "launch", 7 | "name": "Attach to running app (SweetPad)", 8 | "preLaunchTask": "sweetpad: launch" 9 | }, 10 | { 11 | "type": "lldb", 12 | "request": "launch", 13 | "args": [], 14 | "cwd": "${workspaceFolder:screenshot-organizer}", 15 | "name": "Debug ScreenshotOrganizer", 16 | "program": "${workspaceFolder:screenshot-organizer}/.build/debug/ScreenshotOrganizer", 17 | "preLaunchTask": "swift: Build Debug ScreenshotOrganizer" 18 | }, 19 | { 20 | "type": "lldb", 21 | "request": "launch", 22 | "args": [], 23 | "cwd": "${workspaceFolder:screenshot-organizer}", 24 | "name": "Release ScreenshotOrganizer", 25 | "program": "${workspaceFolder:screenshot-organizer}/.build/release/ScreenshotOrganizer", 26 | "preLaunchTask": "swift: Build Release ScreenshotOrganizer" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /project.yml: -------------------------------------------------------------------------------- 1 | name: ScreenshotOrganizer 2 | options: 3 | bundleIdPrefix: com.reorx 4 | deploymentTarget: 5 | macOS: "12.0" 6 | packages: 7 | KeyboardShortcuts: 8 | url: https://github.com/sindresorhus/KeyboardShortcuts 9 | from: 1.0.0 10 | targets: 11 | ScreenshotOrganizer: 12 | type: application 13 | platform: macOS 14 | sources: 15 | - ScreenshotOrganizer 16 | dependencies: 17 | - package: KeyboardShortcuts 18 | settings: 19 | base: 20 | INFOPLIST_FILE: ScreenshotOrganizer/Info.plist 21 | PRODUCT_BUNDLE_IDENTIFIER: com.reorx.ScreenshotOrganizer 22 | CODE_SIGN_ENTITLEMENTS: ScreenshotOrganizer/ScreenshotOrganizer.entitlements 23 | ScreenshotOrganizerLauncher: 24 | type: application 25 | platform: macOS 26 | sources: 27 | - ScreenshotOrganizerLauncher 28 | settings: 29 | base: 30 | INFOPLIST_FILE: ScreenshotOrganizerLauncher/Info.plist 31 | PRODUCT_BUNDLE_IDENTIFIER: com.reorx.ScreenshotOrganizer.Launcher 32 | CODE_SIGN_ENTITLEMENTS: ScreenshotOrganizerLauncher/ScreenshotOrganizerLauncher.entitlements 33 | -------------------------------------------------------------------------------- /dev_notes.md: -------------------------------------------------------------------------------- 1 | ## Invalid Display Identifier Logs 2 | 3 | When running the app for a long time, these logs appear frequently in the Xcode console: 4 | ``` 5 | invalid display identifier A00595AB-E552-4F31-B0F4-A07E3F70E8B2 6 | invalid display identifier 6CED9FBD-C3DB-4309-9DFB-D83D9EE5BD3A 7 | ``` 8 | 9 | ### Cause 10 | These UUIDs are display identifiers that macOS uses to identify screens. The logs appear because: 11 | 12 | 1. macOS assigns a unique identifier to each display in the system 13 | 2. When screenshots are taken, macOS associates them with the display they were captured from 14 | 3. When the app runs for a long time, displays that were previously connected (external monitors, etc.) are no longer available 15 | 4. When macOS tries to reference these display IDs, it logs "invalid display identifier" because those displays are no longer connected 16 | 17 | ### Impact 18 | - These logs are harmless and don't indicate a problem with the app 19 | - They come from the macOS system, not the application code 20 | - They can be safely ignored as they don't affect functionality 21 | 22 | ### Workaround 23 | To prevent these logs from cluttering the console, add a filter for "invalid display identifier" in Xcode's console filter box. 24 | -------------------------------------------------------------------------------- /ScreenshotOrganizerLauncher/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSMinimumSystemVersion 22 | $(MACOSX_DEPLOYMENT_TARGET) 23 | LSUIElement 24 | 25 | SMPrivilegedExecutables 26 | 27 | com.reorx.ScreenshotOrganizer 28 | identifier "com.reorx.ScreenshotOrganizer" 29 | 30 | SMAuthorizedClients 31 | 32 | identifier "com.reorx.ScreenshotOrganizer" 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /ScreenshotOrganizer/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon-16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "icon-32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "icon-32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "icon-64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "icon-128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "icon-256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "icon-256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "icon-512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "icon-512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "icon-1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /ScreenshotOrganizer/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | AppIcon 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.2.0 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2023. All rights reserved. 27 | NSPrincipalClass 28 | NSApplication 29 | LSUIElement 30 | 31 | NSSupportsAutomaticTermination 32 | 33 | NSSupportsSuddenTermination 34 | 35 | SMPrivilegedExecutables 36 | 37 | com.reorx.ScreenshotOrganizer.Launcher 38 | identifier "com.reorx.ScreenshotOrganizer.Launcher" 39 | 40 | SMAuthorizedClients 41 | 42 | identifier "com.reorx.ScreenshotOrganizer.Launcher" 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/github-actions.md: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow Documentation 2 | 3 | ## Build and Package App Workflow 4 | 5 | This workflow automatically builds the ScreenshotOrganizer app and creates artifacts based on the type of commit. 6 | 7 | ### Triggers 8 | 9 | - **Push to branches**: `master`, `develop` 10 | - **Push to tags**: Starting with `v*` (e.g., `v1.2.1`, `v2.0.0`) 11 | - **Pull requests**: To `master` branch 12 | - **Manual trigger**: Via `workflow_dispatch` 13 | 14 | ### Artifact Naming 15 | 16 | The workflow creates different artifact names based on the commit type: 17 | 18 | #### For Tagged Commits 19 | - **Artifact name**: `ScreenshotOrganizer-{tag}` (e.g., `ScreenshotOrganizer-v1.2.1`) 20 | - **Zip file**: `ScreenshotOrganizer-{tag}.zip` (e.g., `ScreenshotOrganizer-v1.2.1.zip`) 21 | - **Contains**: `ScreenshotOrganizer.app` directly (no double-zip) 22 | 23 | #### For Regular Commits 24 | - **Artifact name**: `ScreenshotOrganizer-{sha}` (e.g., `ScreenshotOrganizer-abc123def456`) 25 | - **Zip file**: `ScreenshotOrganizer.zip` 26 | - **Contains**: `ScreenshotOrganizer.app` directly (no double-zip) 27 | 28 | ### Automatic Release Creation 29 | 30 | When a tag starting with `v*` is pushed: 31 | 32 | 1. **Build Process**: App is built and packaged automatically 33 | 2. **Artifact Creation**: Creates `ScreenshotOrganizer-{tag}.zip` 34 | 3. **Release Creation**: Automatically creates a GitHub release with: 35 | - **Name**: `Release {tag}` (e.g., `Release v1.2.1`) 36 | - **Attached file**: The zip artifact 37 | - **Release notes**: Auto-generated from commits 38 | - **Status**: Published (not draft, not prerelease) 39 | 40 | ### Usage Examples 41 | 42 | #### Creating a Release 43 | ```bash 44 | # Tag the current commit 45 | git tag v1.2.1 46 | git push origin v1.2.1 47 | 48 | # This will: 49 | # 1. Trigger the workflow 50 | # 2. Build the app 51 | # 3. Create artifact: ScreenshotOrganizer-v1.2.1 52 | # 4. Create release: "Release v1.2.1" 53 | # 5. Attach ScreenshotOrganizer-v1.2.1.zip to the release 54 | ``` 55 | 56 | #### Development Builds 57 | ```bash 58 | # Push to master or develop 59 | git push origin master 60 | 61 | # This will: 62 | # 1. Trigger the workflow 63 | # 2. Build the app 64 | # 3. Create artifact: ScreenshotOrganizer-{sha} 65 | # 4. No release created 66 | ``` 67 | 68 | ### Key Improvements 69 | 70 | 1. **Professional Naming**: Tagged releases use semantic version names instead of SHA hashes 71 | 2. **Single Zip**: Eliminated confusing double-zip structure 72 | 3. **Automatic Releases**: No manual intervention needed for releases 73 | 4. **Consistent Artifacts**: Clear naming convention for all build types -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Screenshot Organizer 2 | 3 | A macOS menubar app that automatically organizes screenshot files and screen recordings into year/month folders. 4 | 5 | ## Features 6 | 7 | - Runs silently in the background as a menubar app 8 | - Monitors a specified folder (default: Desktop) for new screenshot files and screen recordings 9 | - Automatically moves screenshots to YYYY/MM folders based on the screenshot's date 10 | - Automatically moves screen recordings to recordings/YYYY/MM folders 11 | - Allows changing the monitored folder through the UI 12 | - Organize files on-demand with "Organize now" option 13 | 14 | ## Requirements 15 | 16 | - macOS 12.0 or later 17 | - Xcode 13.0 or later (for building from source) 18 | 19 | ## Installation 20 | 21 | ### Download Pre-built Release 22 | 23 | 1. Download the latest release from the [Releases page](https://github.com/reorx/screenshot-organizer/releases) 24 | 2. Extract the .zip file and drag the app to your Applications folder 25 | 3. Launch the app 26 | 27 | ### Build from GitHub Actions 28 | 29 | Each push to the main branch and every pull request automatically builds the app. You can download the latest build artifact from the [Actions page](https://github.com/reorx/screenshot-organizer/actions). 30 | 31 | **For tagged releases**: When a version tag (e.g., `v1.2.1`) is pushed, the workflow automatically: 32 | - Creates a properly named artifact (`ScreenshotOrganizer-v1.2.1.zip`) 33 | - Creates a GitHub release with the artifact attached 34 | - Generates release notes automatically 35 | 36 | See [GitHub Actions documentation](docs/github-actions.md) for more details. 37 | 38 | ## Building from Source 39 | 40 | 1. Clone the repository 41 | 2. Open `ScreenshotOrganizer.xcodeproj` in Xcode 42 | 3. Build and run the app (⌘+R) 43 | 44 | ## Usage 45 | 46 | 1. Launch the app 47 | 2. The app runs in the background, with an icon in the menubar 48 | 3. Click the icon to access settings 49 | 4. By default, the app monitors your Desktop folder 50 | 5. You can change the monitored folder in the settings 51 | 6. Use "Organize now" from the menu to organize files immediately 52 | 53 | ## How It Works 54 | 55 | When you take a screenshot on macOS, it's saved with a filename in the format: `Screenshot YYYY-MM-DD at HH.MM.SS.png`. The app looks for files matching this pattern and moves them to a folder structure based on the date in the filename. 56 | 57 | For example, a file named `Screenshot 2023-04-15 at 10.30.45.png` would be moved to `~/Desktop/2023/04/Screenshot 2023-04-15 at 10.30.45.png`. 58 | 59 | Similarly, screen recordings with the format `Screen Recording YYYY-MM-DD at HH.MM.SS.mov` are organized into `~/Desktop/recordings/YYYY/MM/Screen Recording YYYY-MM-DD at HH.MM.SS.mov`. 60 | 61 | ## TODO 62 | 63 | - [ ] auto rename screenshots to configured naming template. this feature works independently, regardless of whether the folder organizing feature is enabled or not 64 | - [ ] image compression feature 65 | - [ ] show recent screenshots in the menbar menu dropdown, with sub menu to copy path or image content or open share menu 66 | 67 | ## License 68 | 69 | This project is licensed under the MIT License - see the LICENSE file for details. 70 | -------------------------------------------------------------------------------- /ScreenshotOrganizer/AppLogger.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import OSLog 3 | 4 | public class AppLogger { 5 | public static let shared = AppLogger() 6 | private let fileLogger: FileLogger 7 | private let systemLogger = os.Logger(subsystem: "com.screenshotorganizer", category: "app") 8 | 9 | private init() { 10 | fileLogger = FileLogger() 11 | } 12 | 13 | public func setup(logDirectory: URL) { 14 | fileLogger.setup(logDirectory: logDirectory) 15 | } 16 | 17 | public func info(_ message: String) { 18 | systemLogger.info("\(message)") 19 | fileLogger.log(message, level: .info) 20 | } 21 | 22 | public func error(_ message: String) { 23 | systemLogger.error("\(message)") 24 | fileLogger.log(message, level: .error) 25 | } 26 | 27 | public func debug(_ message: String) { 28 | systemLogger.debug("\(message)") 29 | fileLogger.log(message, level: .debug) 30 | } 31 | 32 | public var logFileURL: URL { 33 | return fileLogger.logFileURL ?? URL(fileURLWithPath: "") 34 | } 35 | } 36 | 37 | public func setupAppLogger(logDirectory: URL) { 38 | AppLogger.shared.setup(logDirectory: logDirectory) 39 | } 40 | 41 | private class FileLogger { 42 | public var logFileURL: URL? 43 | private let dateFormatter: DateFormatter = { 44 | let formatter = DateFormatter() 45 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 46 | return formatter 47 | }() 48 | 49 | func setup(logDirectory: URL) { 50 | AppLogger.shared.info("Setting up file logger with directory: \(logDirectory.path)") 51 | // Create the log directory if it doesn't exist 52 | do { 53 | try FileManager.default.createDirectory(at: logDirectory, withIntermediateDirectories: true) 54 | } catch { 55 | AppLogger.shared.error("Failed to create log directory: \(error)") 56 | } 57 | 58 | let timestamp = DateFormatter.logFileName.string(from: Date()) 59 | logFileURL = logDirectory.appendingPathComponent("screenshot-organizer-\(timestamp).log") 60 | } 61 | 62 | func log(_ message: String, level: LogLevel) { 63 | guard let logFileURL = logFileURL else { return } 64 | 65 | let timestamp = dateFormatter.string(from: Date()) 66 | let logMessage = "[\(timestamp)] [\(level.rawValue)] \(message)\n" 67 | 68 | do { 69 | if !FileManager.default.fileExists(atPath: logFileURL.path) { 70 | try logMessage.write(to: logFileURL, atomically: true, encoding: .utf8) 71 | } else { 72 | let fileHandle = try FileHandle(forWritingTo: logFileURL) 73 | fileHandle.seekToEndOfFile() 74 | fileHandle.write(logMessage.data(using: .utf8)!) 75 | fileHandle.closeFile() 76 | } 77 | } catch { 78 | AppLogger.shared.error("Failed to write to log file: \(error)") 79 | } 80 | } 81 | } 82 | 83 | private enum LogLevel: String { 84 | case info = "INFO" 85 | case error = "ERROR" 86 | case debug = "DEBUG" 87 | } 88 | 89 | extension DateFormatter { 90 | static let logFileName: DateFormatter = { 91 | let formatter = DateFormatter() 92 | formatter.dateFormat = "yyyy-MM-dd" 93 | return formatter 94 | }() 95 | } 96 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Package App 2 | 3 | on: 4 | push: 5 | branches: [ master, develop ] 6 | tags: [ '**' ] 7 | pull_request: 8 | branches: [ master ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | build: 16 | runs-on: macos-latest 17 | 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup Xcode 23 | uses: maxim-lobanov/setup-xcode@v1 24 | with: 25 | xcode-version: latest-stable 26 | 27 | - name: Cache Swift Package Manager 28 | uses: actions/cache@v4 29 | with: 30 | path: | 31 | .build 32 | *.xcodeproj/project.xcworkspace/xcshareddata/swiftpm 33 | key: ${{ runner.os }}-spm-${{ hashFiles('Package.swift', 'Package.resolved') }} 34 | restore-keys: | 35 | ${{ runner.os }}-spm- 36 | 37 | - name: Build app 38 | run: | 39 | # Build the main app first 40 | xcodebuild -project ScreenshotOrganizer.xcodeproj \ 41 | -scheme ScreenshotOrganizer \ 42 | -configuration Release \ 43 | -derivedDataPath build \ 44 | CODE_SIGN_IDENTITY="" \ 45 | CODE_SIGNING_REQUIRED=NO \ 46 | CODE_SIGNING_ALLOWED=NO \ 47 | build 48 | 49 | # Build the launcher app 50 | xcodebuild -project ScreenshotOrganizer.xcodeproj \ 51 | -scheme ScreenshotOrganizerLauncher \ 52 | -configuration Release \ 53 | -derivedDataPath build \ 54 | CODE_SIGN_IDENTITY="" \ 55 | CODE_SIGNING_REQUIRED=NO \ 56 | CODE_SIGNING_ALLOWED=NO \ 57 | build 58 | 59 | - name: Package app 60 | run: | 61 | # List what was built for debugging 62 | echo "Built products:" 63 | ls -la build/Build/Products/Release/ 64 | 65 | # Create release directory 66 | mkdir -p release 67 | 68 | # Copy the main app (which should contain the launcher) 69 | cp -R build/Build/Products/Release/ScreenshotOrganizer.app release/ 70 | 71 | # Verify the app structure 72 | echo "App structure:" 73 | find release/ScreenshotOrganizer.app -type d -name "*Launcher*" || echo "No launcher directory found" 74 | 75 | # Determine the artifact name based on whether this is a tag or not 76 | if [[ "${{ github.ref_type }}" == "tag" ]]; then 77 | ARTIFACT_NAME="ScreenshotOrganizer-${{ github.ref_name }}" 78 | echo "Tagged release artifact will be: $ARTIFACT_NAME" 79 | else 80 | ARTIFACT_NAME="ScreenshotOrganizer-${{ github.sha }}" 81 | echo "Development artifact will be: $ARTIFACT_NAME" 82 | fi 83 | 84 | # Create dir with the determined name 85 | mkdir "$ARTIFACT_NAME" 86 | 87 | # move app to the artifact dir 88 | mv "release/ScreenshotOrganizer.app" "$ARTIFACT_NAME" 89 | 90 | # Show final dir info 91 | ls -ld "$ARTIFACT_NAME" 92 | 93 | # Store the artifact name for later steps 94 | echo "ARTIFACT_NAME=$ARTIFACT_NAME" >> $GITHUB_ENV 95 | 96 | - name: Upload artifact 97 | uses: actions/upload-artifact@v4 98 | with: 99 | name: ${{ github.ref_type == 'tag' && format('ScreenshotOrganizer-{0}', github.ref_name) || format('ScreenshotOrganizer-{0}', github.sha) }} 100 | path: ${{ env.ARTIFACT_NAME }} 101 | retention-days: 30 102 | 103 | - name: Create asset for release (on tag) 104 | if: github.ref_type == 'tag' 105 | run: | 106 | # Create zip file for release asset 107 | zip -r "$ARTIFACT_NAME.zip" "$ARTIFACT_NAME" 108 | 109 | - name: Create release (on tag) 110 | if: github.ref_type == 'tag' 111 | uses: softprops/action-gh-release@v2 112 | with: 113 | files: ${{ env.ARTIFACT_NAME }}.zip 114 | #draft: false 115 | #prerelease: false 116 | generate_release_notes: true 117 | env: 118 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 119 | -------------------------------------------------------------------------------- /ScreenshotOrganizer/SettingsContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import KeyboardShortcuts 3 | 4 | struct SettingsContentView: View { 5 | @AppStorage(SettingsKey.monitoredDirectory) private var monitoredDirectory: String = SettingsDefault.monitoredDirectory 6 | @AppStorage(SettingsKey.logDirectory) private var logDirectory: String = SettingsDefault.logDirectory 7 | @AppStorage(SettingsKey.launchAtLogin) private var launchAtLogin: Bool = SettingsDefault.launchAtLogin 8 | @State private var isDirectoryPickerShown = false 9 | @State private var isLogDirectoryPickerShown = false 10 | @State private var showConfirmationDialog = false 11 | @State private var selectedDirectory: URL? 12 | @State private var selectedLogDirectory: URL? 13 | 14 | var body: some View { 15 | VStack(alignment: .leading, spacing: 16) { 16 | HStack { 17 | Image("icon") 18 | .resizable() 19 | .frame(width: 48, height: 48) 20 | .cornerRadius(8) 21 | 22 | Text("Screenshot Organizer") 23 | .font(.headline) 24 | } 25 | .padding(.bottom, 8) 26 | .frame(maxWidth: .infinity, alignment: .leading) 27 | 28 | VStack(alignment: .leading, spacing: 8) { 29 | Text("Monitored folder:") 30 | .font(.subheadline) 31 | 32 | HStack { 33 | Text(monitoredDirectory) 34 | .truncationMode(.middle) 35 | .lineLimit(1) 36 | .frame(maxWidth: .infinity, alignment: .leading) 37 | 38 | Button("Change") { 39 | isDirectoryPickerShown = true 40 | } 41 | .fileImporter( 42 | isPresented: $isDirectoryPickerShown, 43 | allowedContentTypes: [.folder], 44 | onCompletion: handleDirectorySelection 45 | ) 46 | } 47 | .padding(8) 48 | .background(Color(.textBackgroundColor)) 49 | .cornerRadius(4) 50 | 51 | Text(monitoredDirectory) 52 | .font(.caption) 53 | .foregroundColor(.secondary) 54 | .lineLimit(1) 55 | .truncationMode(.middle) 56 | } 57 | 58 | VStack(alignment: .leading, spacing: 8) { 59 | Text("Log folder:") 60 | .font(.subheadline) 61 | 62 | HStack { 63 | Text(logDirectory) 64 | .truncationMode(.middle) 65 | .lineLimit(1) 66 | .frame(maxWidth: .infinity, alignment: .leading) 67 | 68 | Button("Change") { 69 | isLogDirectoryPickerShown = true 70 | } 71 | .fileImporter( 72 | isPresented: $isLogDirectoryPickerShown, 73 | allowedContentTypes: [.folder], 74 | onCompletion: handleLogDirectorySelection 75 | ) 76 | } 77 | .padding(8) 78 | .background(Color(.textBackgroundColor)) 79 | .cornerRadius(4) 80 | 81 | Text(logDirectory) 82 | .font(.caption) 83 | .foregroundColor(.secondary) 84 | .lineLimit(1) 85 | .truncationMode(.middle) 86 | } 87 | 88 | Toggle("Launch at login", isOn: $launchAtLogin) 89 | .padding(.top, 8) 90 | 91 | VStack(alignment: .leading, spacing: 12) { 92 | Text("Keyboard Shortcuts:") 93 | .font(.subheadline) 94 | .padding(.top, 4) 95 | 96 | HStack { 97 | Text("Open Screenshots Folder:") 98 | .frame(width: 200, alignment: .leading) 99 | KeyboardShortcuts.Recorder(for: .openScreenshotsFolder) 100 | } 101 | 102 | HStack { 103 | Text("Open Screen Recordings Folder:") 104 | .frame(width: 200, alignment: .leading) 105 | KeyboardShortcuts.Recorder(for: .openScreenRecordingsFolder) 106 | } 107 | } 108 | .padding(.top, 12) 109 | 110 | Spacer() 111 | 112 | Button("Quit") { 113 | NSApplication.shared.terminate(nil) 114 | } 115 | .frame(maxWidth: .infinity, alignment: .trailing) 116 | } 117 | .padding() 118 | .alert("Organize folder now?", isPresented: $showConfirmationDialog) { 119 | Button("Yes") { 120 | NotificationCenter.default.post( 121 | name: Notification.Name("OrganizeNow"), 122 | object: nil 123 | ) 124 | } 125 | Button("No", role: .cancel) { 126 | } 127 | } message: { 128 | Text("Would you like to organize the selected folder now?") 129 | } 130 | } 131 | 132 | private func handleDirectorySelection(result: Result) { 133 | switch result { 134 | case .success(let url): 135 | if url.startAccessingSecurityScopedResource() { 136 | monitoredDirectory = url.path 137 | showConfirmationDialog = true 138 | NotificationCenter.default.post( 139 | name: Notification.Name("MonitoredDirectoryChanged"), 140 | object: nil, 141 | userInfo: ["directory": url] 142 | ) 143 | url.stopAccessingSecurityScopedResource() 144 | } 145 | case .failure(let error): 146 | AppLogger.shared.error("Directory selection failed: \(error)") 147 | } 148 | } 149 | 150 | private func handleLogDirectorySelection(result: Result) { 151 | switch result { 152 | case .success(let url): 153 | if url.startAccessingSecurityScopedResource() { 154 | logDirectory = url.path 155 | NotificationCenter.default.post( 156 | name: Notification.Name("LogDirectoryChanged"), 157 | object: nil, 158 | userInfo: ["directory": url] 159 | ) 160 | url.stopAccessingSecurityScopedResource() 161 | } 162 | case .failure(let error): 163 | AppLogger.shared.error("Log directory selection failed: \(error)") 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /ScreenshotOrganizer/FileMonitor.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Cocoa 3 | 4 | enum FileMonitorError: Error { 5 | case directoryNotFound 6 | case fileSystemError(String) 7 | case invalidScreenshot 8 | case moveFailed(String) 9 | case permissionDenied(String) 10 | } 11 | 12 | class FileMonitor { 13 | private let directoryURL: URL 14 | private var directoryMonitor: DispatchSourceFileSystemObject? 15 | private let screenshotRegex = try! NSRegularExpression(pattern: "Screenshot (\\d{4})-(\\d{2})-(\\d{2}) at .+\\.png", options: []) 16 | private let recordingRegex = try! NSRegularExpression(pattern: "Screen Recording (\\d{4})-(\\d{2})-(\\d{2}) at .+\\.mov", options: []) 17 | private var fileSystemEventStream: FSEventStreamRef? 18 | var isMonitoring: Bool = false 19 | private let fileSystemEventCallback: FSEventStreamCallback = { (stream, contextInfo, numEvents, eventPaths, eventFlags, eventIds) in 20 | let fileMonitor = Unmanaged.fromOpaque(contextInfo!).takeUnretainedValue() 21 | fileMonitor.checkForNewScreenshots() 22 | } 23 | 24 | init(directoryURL: URL) { 25 | self.directoryURL = directoryURL 26 | } 27 | 28 | func organizeNow(directoryURL: URL? = nil) throws { 29 | let targetURL = directoryURL ?? self.directoryURL 30 | AppLogger.shared.info("Starting to organize directory: \(targetURL.path)") 31 | 32 | try checkDirectoryAccess(for: targetURL) 33 | 34 | let fileManager = FileManager.default 35 | guard fileManager.fileExists(atPath: targetURL.path) else { 36 | print("Directory does not exist: \(targetURL.path)") 37 | throw FileMonitorError.directoryNotFound 38 | } 39 | do { 40 | let fileURLs = try fileManager.contentsOfDirectory( 41 | at: targetURL, 42 | includingPropertiesForKeys: [.creationDateKey], 43 | options: [.skipsHiddenFiles] 44 | ) 45 | 46 | for fileURL in fileURLs { 47 | if isScreenshot(fileURL: fileURL) { 48 | try organizeScreenshot(fileURL: fileURL) 49 | } else if isScreenRecording(fileURL: fileURL) { 50 | try organizeScreenRecording(fileURL: fileURL) 51 | } 52 | } 53 | AppLogger.shared.info("Finished organizing directory") 54 | } catch { 55 | print("Error organizing directory: \(error)") 56 | throw FileMonitorError.fileSystemError(error.localizedDescription) 57 | } 58 | } 59 | 60 | func startMonitoring() throws { 61 | guard !isMonitoring else { return } 62 | AppLogger.shared.info("Starting to monitor directory: \(directoryURL.path)") 63 | 64 | try checkDirectoryAccess(for: directoryURL) 65 | 66 | guard FileManager.default.fileExists(atPath: directoryURL.path) else { 67 | throw NSError(domain: "FileMonitor", code: 1, userInfo: [NSLocalizedDescriptionKey: "Directory does not exist"]) 68 | } 69 | 70 | var context = FSEventStreamContext( 71 | version: 0, 72 | info: Unmanaged.passUnretained(self).toOpaque(), 73 | retain: nil, 74 | release: nil, 75 | copyDescription: nil 76 | ) 77 | 78 | fileSystemEventStream = FSEventStreamCreate( 79 | nil, 80 | fileSystemEventCallback, 81 | &context, 82 | [directoryURL.path] as CFArray, 83 | FSEventStreamEventId(kFSEventStreamEventIdSinceNow), 84 | 1.0, 85 | FSEventStreamCreateFlags(kFSEventStreamCreateFlagFileEvents) 86 | ) 87 | 88 | if let stream = fileSystemEventStream { 89 | FSEventStreamSetDispatchQueue(stream, DispatchQueue.main) 90 | FSEventStreamStart(stream) 91 | isMonitoring = true 92 | } else { 93 | throw NSError(domain: "FileMonitor", code: 1, userInfo: [NSLocalizedDescriptionKey: "Could not create file system event stream"]) 94 | } 95 | } 96 | 97 | private func checkForNewScreenshots() { 98 | let fileManager = FileManager.default 99 | 100 | do { 101 | let fileURLs = try fileManager.contentsOfDirectory( 102 | at: directoryURL, 103 | includingPropertiesForKeys: [.creationDateKey], 104 | options: [.skipsHiddenFiles] 105 | ) 106 | 107 | for fileURL in fileURLs { 108 | if isScreenshot(fileURL: fileURL) { 109 | try organizeScreenshot(fileURL: fileURL) 110 | } else if isScreenRecording(fileURL: fileURL) { 111 | try organizeScreenRecording(fileURL: fileURL) 112 | } 113 | } 114 | } catch { 115 | AppLogger.shared.error("Error scanning directory: \(error)") 116 | } 117 | } 118 | 119 | private func isScreenshot(fileURL: URL) -> Bool { 120 | let filename = fileURL.lastPathComponent 121 | let range = NSRange(location: 0, length: filename.utf16.count) 122 | let matches = screenshotRegex.matches(in: filename, options: [], range: range) 123 | return !matches.isEmpty 124 | } 125 | 126 | private func isScreenRecording(fileURL: URL) -> Bool { 127 | let filename = fileURL.lastPathComponent 128 | let range = NSRange(location: 0, length: filename.utf16.count) 129 | let matches = recordingRegex.matches(in: filename, options: [], range: range) 130 | return !matches.isEmpty 131 | } 132 | 133 | private func organizeFileByDate(fileURL: URL, baseURL: URL, year: String, month: String, logPrefix: String = "") throws { 134 | let filename = fileURL.lastPathComponent 135 | let destinationFolderURL = baseURL.appendingPathComponent(year).appendingPathComponent(month) 136 | let fileManager = FileManager.default 137 | 138 | do { 139 | try fileManager.createDirectory(at: destinationFolderURL, withIntermediateDirectories: true) 140 | AppLogger.shared.info("Created directory: \(destinationFolderURL.path)") 141 | 142 | var destinationFileURL = destinationFolderURL.appendingPathComponent(filename) 143 | var counter = 1 144 | 145 | while fileManager.fileExists(atPath: destinationFileURL.path) { 146 | let nameWithoutExt = (filename as NSString).deletingPathExtension 147 | let ext = (filename as NSString).pathExtension 148 | destinationFileURL = destinationFolderURL.appendingPathComponent("\(nameWithoutExt) (\(counter)).\(ext)") 149 | counter += 1 150 | } 151 | 152 | try fileManager.moveItem(at: fileURL, to: destinationFileURL) 153 | AppLogger.shared.info("Moved \(filename) to \(logPrefix)\(year)/\(month)/") 154 | } catch { 155 | throw FileMonitorError.moveFailed(error.localizedDescription) 156 | } 157 | } 158 | 159 | private func organizeScreenRecording(fileURL: URL) throws { 160 | let filename = fileURL.lastPathComponent 161 | let range = NSRange(location: 0, length: filename.utf16.count) 162 | 163 | guard let match = recordingRegex.firstMatch(in: filename, options: [], range: range) else { 164 | throw FileMonitorError.invalidScreenshot 165 | } 166 | 167 | let yearRange = Range(match.range(at: 1), in: filename)! 168 | let monthRange = Range(match.range(at: 2), in: filename)! 169 | 170 | let year = String(filename[yearRange]) 171 | let month = String(filename[monthRange]) 172 | 173 | // Create recordings directory under the main directory 174 | let recordingsBaseURL = directoryURL.appendingPathComponent("recordings") 175 | try organizeFileByDate(fileURL: fileURL, baseURL: recordingsBaseURL, year: year, month: month, logPrefix: "recordings/") 176 | } 177 | 178 | private func organizeScreenshot(fileURL: URL) throws { 179 | let filename = fileURL.lastPathComponent 180 | let range = NSRange(location: 0, length: filename.utf16.count) 181 | 182 | guard let match = screenshotRegex.firstMatch(in: filename, options: [], range: range) else { 183 | throw FileMonitorError.invalidScreenshot 184 | } 185 | 186 | let yearRange = Range(match.range(at: 1), in: filename)! 187 | let monthRange = Range(match.range(at: 2), in: filename)! 188 | 189 | let year = String(filename[yearRange]) 190 | let month = String(filename[monthRange]) 191 | 192 | try organizeFileByDate(fileURL: fileURL, baseURL: directoryURL, year: year, month: month) 193 | } 194 | 195 | func stopMonitoring() { 196 | guard isMonitoring else { return } 197 | if let stream = fileSystemEventStream { 198 | FSEventStreamStop(stream) 199 | FSEventStreamInvalidate(stream) 200 | FSEventStreamRelease(stream) 201 | fileSystemEventStream = nil 202 | isMonitoring = false 203 | } 204 | } 205 | 206 | /// Checks for and requests access to the given directory 207 | private func checkDirectoryAccess(for directoryURL: URL) throws { 208 | // Check if we can access the directory 209 | do { 210 | // Try to list contents as a simple access test 211 | let _ = try FileManager.default.contentsOfDirectory(at: directoryURL, includingPropertiesForKeys: nil) 212 | } catch let error as NSError { 213 | // If access is denied, try to request permission 214 | if error.domain == NSCocoaErrorDomain && 215 | (error.code == 257 || error.code == 260 || error.code == 1) { 216 | 217 | AppLogger.shared.info("Requesting access permission for: \(directoryURL.path)") 218 | 219 | // Request access using NSOpenPanel as a workaround 220 | let openPanel = NSOpenPanel() 221 | openPanel.message = "Grant access to \(directoryURL.lastPathComponent) folder to organize screenshots" 222 | openPanel.prompt = "Grant Access" 223 | openPanel.directoryURL = directoryURL 224 | openPanel.canChooseDirectories = true 225 | openPanel.canChooseFiles = false 226 | openPanel.canCreateDirectories = false 227 | openPanel.allowsMultipleSelection = false 228 | 229 | let response = openPanel.runModal() 230 | if response == .OK, let selectedURL = openPanel.url { 231 | // Access granted, try the operation again 232 | // The security scoped bookmark may be needed for future sessions 233 | let _ = selectedURL.startAccessingSecurityScopedResource() 234 | AppLogger.shared.info("Access granted for: \(selectedURL.path)") 235 | } else { 236 | AppLogger.shared.error("Access denied for: \(directoryURL.path)") 237 | throw FileMonitorError.permissionDenied("Permission denied to access \(directoryURL.path). Please grant access in System Settings > Privacy & Security > Files and Folders.") 238 | } 239 | } else { 240 | throw FileMonitorError.fileSystemError(error.localizedDescription) 241 | } 242 | } 243 | } 244 | 245 | // MARK: - Public folder access methods 246 | 247 | /// Returns the URL for the folder containing screenshots for the current date 248 | func getCurrentScreenshotsFolder() -> URL { 249 | let dateComponents = Calendar.current.dateComponents([.year, .month], from: Date()) 250 | let year = String(dateComponents.year!) 251 | let month = String(format: "%02d", dateComponents.month!) 252 | 253 | let screenshotsFolder = directoryURL.appendingPathComponent(year).appendingPathComponent(month) 254 | 255 | // Create the directory if it doesn't exist 256 | do { 257 | try FileManager.default.createDirectory(at: screenshotsFolder, withIntermediateDirectories: true) 258 | } catch { 259 | AppLogger.shared.error("Failed to create screenshots directory: \(error.localizedDescription)") 260 | } 261 | 262 | return screenshotsFolder 263 | } 264 | 265 | /// Returns the URL for the folder containing screen recordings for the current date 266 | func getCurrentScreenRecordingsFolder() -> URL { 267 | let dateComponents = Calendar.current.dateComponents([.year, .month], from: Date()) 268 | let year = String(dateComponents.year!) 269 | let month = String(format: "%02d", dateComponents.month!) 270 | 271 | let recordingsBaseURL = directoryURL.appendingPathComponent("recordings") 272 | let recordingsFolder = recordingsBaseURL.appendingPathComponent(year).appendingPathComponent(month) 273 | 274 | // Create the directory if it doesn't exist 275 | do { 276 | try FileManager.default.createDirectory(at: recordingsFolder, withIntermediateDirectories: true) 277 | } catch { 278 | AppLogger.shared.error("Failed to create recordings directory: \(error.localizedDescription)") 279 | } 280 | 281 | return recordingsFolder 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /ScreenshotOrganizer/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import SwiftUI 3 | import ServiceManagement 4 | import KeyboardShortcuts 5 | 6 | class AppDelegate: NSObject, NSApplicationDelegate { 7 | private var statusItem: NSStatusItem! 8 | private var fileMonitor: FileMonitor! 9 | private var statusMenu: NSMenu! 10 | private var settingsWindowController: NSWindowController? 11 | private var aboutWindowController: NSWindowController? 12 | @AppStorage(SettingsKey.monitoredDirectory) private var monitoredDirectory: String = SettingsDefault.monitoredDirectory 13 | @AppStorage(SettingsKey.logDirectory) private var logDirectory: String = SettingsDefault.logDirectory 14 | @AppStorage(SettingsKey.launchAtLogin) private var launchAtLogin: Bool = SettingsDefault.launchAtLogin { 15 | didSet { 16 | updateLaunchAtLogin() 17 | } 18 | } 19 | 20 | private enum Window { 21 | static let width: CGFloat = 400 22 | static let height: CGFloat = 450 23 | } 24 | 25 | // private func findDesktopDirectory() -> URL? { 26 | // // First try the regular Desktop directory 27 | // let regularDesktop = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Desktop") 28 | // if FileManager.default.fileExists(atPath: regularDesktop.path) { 29 | // return regularDesktop 30 | // } 31 | // return nil 32 | // } 33 | 34 | private func startFileMonitoring() { 35 | do { 36 | try fileMonitor.startMonitoring() 37 | AppLogger.shared.info("File monitoring started") 38 | } catch { 39 | showDirectoryNotFoundAlert(error: error) 40 | AppLogger.shared.error("Failed to start file monitoring: \(error.localizedDescription)") 41 | } 42 | updateMenubar() 43 | } 44 | 45 | private func stopFileMonitoring() { 46 | fileMonitor.stopMonitoring() 47 | AppLogger.shared.info("File monitoring stopped") 48 | updateMenubar() 49 | } 50 | 51 | func applicationDidFinishLaunching(_ notification: Notification) { 52 | // Initialize the logger 53 | setupAppLogger(logDirectory: URL(fileURLWithPath: logDirectory)) 54 | AppLogger.shared.info("Screenshot Organizer started") 55 | 56 | // Initialize the file monitor with the saved or default directory 57 | fileMonitor = FileMonitor(directoryURL: URL(fileURLWithPath: monitoredDirectory)) 58 | 59 | // Set up the menubar 60 | setupMenubar() 61 | 62 | // Set up notification for directory changes 63 | NotificationCenter.default.addObserver( 64 | self, 65 | selector: #selector(handleMonitoredDirectoryChange), 66 | name: Notification.Name("MonitoredDirectoryChanged"), 67 | object: nil 68 | ) 69 | 70 | // Set up notification for log directory changes 71 | NotificationCenter.default.addObserver( 72 | self, 73 | selector: #selector(handleLogDirectoryChange), 74 | name: Notification.Name("LogDirectoryChanged"), 75 | object: nil 76 | ) 77 | 78 | // Start file monitoring 79 | startFileMonitoring() 80 | 81 | // Update launch at login status 82 | updateLaunchAtLogin() 83 | 84 | // Setup keyboard shortcuts 85 | setupKeyboardShortcuts() 86 | } 87 | 88 | private func showDirectoryNotFoundAlert(error: Error) { 89 | let alert = NSAlert() 90 | alert.messageText = "Directory Not Found" 91 | alert.informativeText = "Could not monitor the directory. Monitoring has been disabled. Error: \(error.localizedDescription)" 92 | alert.alertStyle = .warning 93 | alert.addButton(withTitle: "OK") 94 | alert.runModal() 95 | } 96 | 97 | @objc private func handleMonitoredDirectoryChange(notification: Notification) { 98 | if let userInfo = notification.userInfo, 99 | let directoryURL = userInfo["directory"] as? URL { 100 | fileMonitor.stopMonitoring() 101 | fileMonitor = FileMonitor(directoryURL: directoryURL) 102 | startFileMonitoring() 103 | } 104 | } 105 | 106 | @objc private func handleLogDirectoryChange(notification: Notification) { 107 | if let userInfo = notification.userInfo, 108 | let directoryURL = userInfo["directory"] as? URL { 109 | setupAppLogger(logDirectory: directoryURL) 110 | AppLogger.shared.info("Log directory changed to: \(directoryURL.path)") 111 | } 112 | } 113 | 114 | private func updateMenubar() { 115 | let isOn = fileMonitor.isMonitoring 116 | print("Update menubar: isOn = \(isOn)") 117 | 118 | // Update status item icon and title 119 | if let button = statusItem.button { 120 | let image = NSImage(systemSymbolName: "photo", accessibilityDescription: "Screenshot Organizer") 121 | image?.isTemplate = true 122 | 123 | if isOn { 124 | button.image = image 125 | button.imagePosition = .imageLeft 126 | button.title = "" 127 | button.alphaValue = 1.0 // Make the button fully opaque 128 | } else { 129 | button.image = image 130 | button.imagePosition = .imageLeft 131 | button.title = "" 132 | button.alphaValue = 0.5 // Dim the button 133 | } 134 | } 135 | 136 | // Update app name with monitoring status 137 | if let appNameItem = statusMenu?.item(at: 0) as? NSMenuItem { 138 | appNameItem.title = "Screenshot Organizer is \(isOn ? "ON" : "OFF")" 139 | } 140 | 141 | // Update turn on/off item 142 | if let menu = statusMenu, menu.items.count > 2 { 143 | menu.item(at: 2)?.title = isOn ? "Turn off" : "Turn on" 144 | } 145 | } 146 | 147 | private func setupMenubar() { 148 | // status bar button 149 | statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) 150 | 151 | if let button = statusItem.button { 152 | let image = NSImage(systemSymbolName: "photo", accessibilityDescription: "Screenshot Organizer") 153 | image?.isTemplate = true 154 | button.image = image 155 | } 156 | 157 | // menu 158 | statusMenu = NSMenu() 159 | 160 | // App name (disabled item) 161 | let appNameItem = NSMenuItem(title: "Screenshot Organizer", action: nil, keyEquivalent: "") 162 | appNameItem.isEnabled = false 163 | statusMenu.addItem(appNameItem) 164 | 165 | // Separator 166 | statusMenu.addItem(NSMenuItem.separator()) 167 | 168 | // Monitoring status item 169 | let monitoringStatusItem = NSMenuItem( 170 | title: fileMonitor.isMonitoring ? "Turn off" : "Turn on", 171 | action: #selector(toggleMonitoring), 172 | keyEquivalent: "" 173 | ) 174 | statusMenu.addItem(monitoringStatusItem) 175 | 176 | // Organize now item 177 | let organizeNowItem = NSMenuItem(title: "Organize now", action: #selector(organizeNow), keyEquivalent: "") 178 | statusMenu.addItem(organizeNowItem) 179 | 180 | // Settings item 181 | statusMenu.addItem(NSMenuItem.separator()) 182 | let settingsItem = NSMenuItem(title: "Settings", action: #selector(showSettings), keyEquivalent: ",") 183 | statusMenu.addItem(settingsItem) 184 | 185 | // About item 186 | let aboutItem = NSMenuItem(title: "About", action: #selector(showAbout), keyEquivalent: "") 187 | statusMenu.addItem(aboutItem) 188 | 189 | // Separator for folders section 190 | statusMenu.addItem(NSMenuItem.separator()) 191 | 192 | // Open screenshots folder item 193 | let screenshotsFolderItem = NSMenuItem( 194 | title: "Current Screenshots Folder", 195 | action: #selector(openScreenshotsFolder), 196 | keyEquivalent: "" 197 | ) 198 | statusMenu.addItem(screenshotsFolderItem) 199 | 200 | // Open screen recordings folder item 201 | let recordingsFolderItem = NSMenuItem( 202 | title: "Current Screenrecordings Folder", 203 | action: #selector(openScreenRecordingsFolder), 204 | keyEquivalent: "" 205 | ) 206 | statusMenu.addItem(recordingsFolderItem) 207 | 208 | // Separator 209 | statusMenu.addItem(NSMenuItem.separator()) 210 | 211 | // Quit item 212 | let quitItem = NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q") 213 | statusMenu.addItem(quitItem) 214 | 215 | // Assign the menu to the status item 216 | statusItem.menu = statusMenu 217 | 218 | // Set up notification for organize now 219 | NotificationCenter.default.addObserver( 220 | self, 221 | selector: #selector(handleOrganizeNow), 222 | name: Notification.Name("OrganizeNow"), 223 | object: nil 224 | ) 225 | } 226 | 227 | @objc private func toggleMonitoring() { 228 | if fileMonitor.isMonitoring { 229 | stopFileMonitoring() 230 | } else { 231 | startFileMonitoring() 232 | } 233 | } 234 | 235 | @objc private func showSettings() { 236 | // If we already have a settings window, just bring it to front 237 | if let windowController = settingsWindowController, 238 | let window = windowController.window { 239 | window.makeKeyAndOrderFront(nil) 240 | NSApp.activate(ignoringOtherApps: true) 241 | return 242 | } 243 | 244 | // Otherwise create a new window 245 | let settingsWindow = NSWindow( 246 | contentRect: NSRect(x: 0, y: 0, width: Window.width, height: Window.height), 247 | styleMask: [.titled, .closable], 248 | backing: .buffered, 249 | defer: false 250 | ) 251 | settingsWindow.setFrame(NSRect(x: 0, y: 0, width: Window.width, height: Window.height), display: true) 252 | settingsWindow.center() 253 | settingsWindow.title = "Screenshot Organizer Settings" 254 | settingsWindow.contentView = NSHostingView(rootView: SettingsContentView()) 255 | 256 | settingsWindowController = NSWindowController(window: settingsWindow) 257 | settingsWindowController?.showWindow(nil) 258 | settingsWindow.makeKeyAndOrderFront(nil) 259 | 260 | NSApp.activate(ignoringOtherApps: true) 261 | } 262 | 263 | private func showErrorAlert(title: String, message: String) { 264 | let alert = NSAlert() 265 | alert.messageText = title 266 | alert.informativeText = message 267 | alert.alertStyle = .warning 268 | alert.addButton(withTitle: "OK") 269 | alert.runModal() 270 | } 271 | 272 | @objc private func handleOrganizeNow(notification: Notification) { 273 | organizeNow() 274 | } 275 | 276 | @objc private func organizeNow() { 277 | do { 278 | try fileMonitor.organizeNow() 279 | } catch FileMonitorError.directoryNotFound { 280 | showErrorAlert(title: "Directory Not Found", message: "The target directory does not exist.") 281 | } catch FileMonitorError.fileSystemError(let message) { 282 | showErrorAlert(title: "File System Error", message: "An error occurred while accessing the file system: \(message)") 283 | } catch FileMonitorError.invalidScreenshot { 284 | showErrorAlert(title: "Invalid Screenshot", message: "One or more files could not be identified as screenshots.") 285 | } catch FileMonitorError.moveFailed(let message) { 286 | showErrorAlert(title: "Move Failed", message: "Failed to move screenshot: \(message)") 287 | } catch FileMonitorError.permissionDenied(let message) { 288 | showErrorAlert(title: "Permission Error", message: message) 289 | } catch { 290 | showErrorAlert(title: "Error", message: "An unexpected error occurred: \(error.localizedDescription)") 291 | } 292 | } 293 | 294 | @objc private func showLog() { 295 | NSWorkspace.shared.open(AppLogger.shared.logFileURL) 296 | } 297 | 298 | @objc private func showAbout() { 299 | // If we already have an about window, just bring it to front 300 | if let windowController = aboutWindowController, 301 | let window = windowController.window { 302 | window.makeKeyAndOrderFront(nil) 303 | NSApp.activate(ignoringOtherApps: true) 304 | return 305 | } 306 | 307 | // Otherwise create a new window 308 | let aboutWindow = NSWindow( 309 | contentRect: NSRect(x: 0, y: 0, width: 300, height: 250), 310 | styleMask: [.titled, .closable], 311 | backing: .buffered, 312 | defer: false 313 | ) 314 | aboutWindow.center() 315 | aboutWindow.title = "About Screenshot Organizer" 316 | aboutWindow.contentView = NSHostingView(rootView: AboutView()) 317 | 318 | aboutWindowController = NSWindowController(window: aboutWindow) 319 | aboutWindowController?.showWindow(nil) 320 | aboutWindow.makeKeyAndOrderFront(nil) 321 | 322 | NSApp.activate(ignoringOtherApps: true) 323 | } 324 | 325 | func applicationWillTerminate(_ notification: Notification) { 326 | fileMonitor.stopMonitoring() 327 | } 328 | 329 | private func updateLaunchAtLogin() { 330 | if #available(macOS 13.0, *) { 331 | Task { 332 | do { 333 | if launchAtLogin { 334 | try SMAppService.mainApp.register() 335 | } else { 336 | try SMAppService.mainApp.unregister() 337 | } 338 | AppLogger.shared.info("Launch at login \(launchAtLogin ? "enabled" : "disabled")") 339 | } catch { 340 | AppLogger.shared.error("Failed to \(launchAtLogin ? "enable" : "disable") launch at login: \(error.localizedDescription)") 341 | } 342 | } 343 | } else { 344 | AppLogger.shared.info("Fallback to legacy launch at login") 345 | // Fallback for older macOS versions (macOS 12) 346 | let identifier = "com.reorx.ScreenshotOrganizer.Launcher" as CFString 347 | if launchAtLogin { 348 | if !SMLoginItemSetEnabled(identifier, true) { 349 | AppLogger.shared.error("Failed to enable launch at login") 350 | } 351 | } else { 352 | if !SMLoginItemSetEnabled(identifier, false) { 353 | AppLogger.shared.error("Failed to disable launch at login") 354 | } 355 | } 356 | } 357 | } 358 | 359 | private func setupKeyboardShortcuts() { 360 | KeyboardShortcuts.onKeyUp(for: .openScreenshotsFolder) { [weak self] in 361 | guard let self = self else { return } 362 | self.openScreenshotsFolder() 363 | } 364 | 365 | KeyboardShortcuts.onKeyUp(for: .openScreenRecordingsFolder) { [weak self] in 366 | guard let self = self else { return } 367 | self.openScreenRecordingsFolder() 368 | } 369 | } 370 | 371 | @objc private func openScreenshotsFolder() { 372 | let folder = fileMonitor.getCurrentScreenshotsFolder() 373 | NSWorkspace.shared.open(folder) 374 | AppLogger.shared.info("Opened screenshots folder: \(folder.path)") 375 | } 376 | 377 | @objc private func openScreenRecordingsFolder() { 378 | let folder = fileMonitor.getCurrentScreenRecordingsFolder() 379 | NSWorkspace.shared.open(folder) 380 | AppLogger.shared.info("Opened screen recordings folder: \(folder.path)") 381 | } 382 | } 383 | 384 | 385 | struct AboutView: View { 386 | @State private var showingHelloAlert = false 387 | 388 | var body: some View { 389 | VStack(spacing: 16) { 390 | Image("icon") 391 | .resizable() 392 | .frame(width: 64, height: 64) 393 | .cornerRadius(8) 394 | 395 | Text("Screenshot Organizer") 396 | .font(.headline) 397 | .bold() 398 | 399 | Text(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Version Unknown") 400 | .font(.subheadline) 401 | 402 | Divider() 403 | 404 | Text("Created by Reorx") 405 | .font(.body) 406 | 407 | HStack { 408 | Text("Website:") 409 | Link("reorx.com", destination: URL(string: "https://reorx.com")!) 410 | .foregroundColor(.blue) 411 | } 412 | 413 | Spacer() 414 | 415 | HStack { 416 | Button("Say Hello") { 417 | showingHelloAlert = true 418 | } 419 | .alert("Hello!", isPresented: $showingHelloAlert) { 420 | Button("OK", role: .cancel) { } 421 | } 422 | 423 | Spacer() 424 | 425 | Button("Close") { 426 | NSApp.keyWindow?.close() 427 | } 428 | } 429 | } 430 | .frame(width: 300, height: 250) 431 | .padding() 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /ScreenshotOrganizer.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 2D412C08CB31202D939B3AE5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAC25A121C87606D74433D2F /* Assets.xcassets */; }; 11 | 475CD581BFB94856E6344294 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B3DB258197C6BBD4EE983D /* AppDelegate.swift */; }; 12 | 4ADDA6D17B56CFA588B5A757 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F1011AB6F8387A658546FA /* main.swift */; }; 13 | 54508DE0D293274A8807655C /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0959B6B8684EC41A4C1C1E4F /* Constants.swift */; }; 14 | 71999DFEB2CE5396B45A90E5 /* KeyboardShortcuts in Frameworks */ = {isa = PBXBuildFile; productRef = 50A1B56541822A866684DEEB /* KeyboardShortcuts */; }; 15 | 7BBBAD5AC2F29166995B6D85 /* AppLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AD13073E20B6F007EB27850 /* AppLogger.swift */; }; 16 | A59220CD130A1070D2C0A8FD /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9DE15EF095B4D72E2995B4D8 /* Preview Assets.xcassets */; }; 17 | AEF9EC7B1F82BCE5C556F458 /* SettingsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683E1C0A1A5B2422E2044FD0 /* SettingsContentView.swift */; }; 18 | DA49A14B6FB9F2B487241EFD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8318E21DF06EFAC8B995CBA /* main.swift */; }; 19 | F3053729E386DF10D38913A5 /* FileMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 969CDD22B665D91F5AA9FC20 /* FileMonitor.swift */; }; 20 | F9DE0C064B87FD976AE381C9 /* KeyboardShortcutNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C9F70F9EFB75F4672B49578 /* KeyboardShortcutNames.swift */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXFileReference section */ 24 | 0959B6B8684EC41A4C1C1E4F /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 25 | 2856C72A01787D1B6859310B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 26 | 2B0729CC69F3D53BF8941E9C /* ScreenshotOrganizerLauncher.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ScreenshotOrganizerLauncher.entitlements; sourceTree = ""; }; 27 | 3D9318DA5CC071CF1DA9AD6B /* ScreenshotOrganizer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ScreenshotOrganizer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | 470F50A7532E508B244A2F3C /* ScreenshotOrganizerLauncher.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ScreenshotOrganizerLauncher.app; sourceTree = BUILT_PRODUCTS_DIR; }; 29 | 683E1C0A1A5B2422E2044FD0 /* SettingsContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsContentView.swift; sourceTree = ""; }; 30 | 7AD13073E20B6F007EB27850 /* AppLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLogger.swift; sourceTree = ""; }; 31 | 8C9F70F9EFB75F4672B49578 /* KeyboardShortcutNames.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardShortcutNames.swift; sourceTree = ""; }; 32 | 969CDD22B665D91F5AA9FC20 /* FileMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMonitor.swift; sourceTree = ""; }; 33 | 9DE15EF095B4D72E2995B4D8 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 34 | A4B3DB258197C6BBD4EE983D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 35 | A8318E21DF06EFAC8B995CBA /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 36 | C0F1011AB6F8387A658546FA /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 37 | C36F5CBF7F34250B6EC66D6C /* ScreenshotOrganizer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ScreenshotOrganizer.entitlements; sourceTree = ""; }; 38 | CAC25A121C87606D74433D2F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 39 | D7105D43346BA15BCA978F3E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 40 | /* End PBXFileReference section */ 41 | 42 | /* Begin PBXFrameworksBuildPhase section */ 43 | A032DA31A0F7B0882268EF2B /* Frameworks */ = { 44 | isa = PBXFrameworksBuildPhase; 45 | buildActionMask = 2147483647; 46 | files = ( 47 | 71999DFEB2CE5396B45A90E5 /* KeyboardShortcuts in Frameworks */, 48 | ); 49 | runOnlyForDeploymentPostprocessing = 0; 50 | }; 51 | /* End PBXFrameworksBuildPhase section */ 52 | 53 | /* Begin PBXGroup section */ 54 | 39F536E9DE81EF8286DB1B07 /* Preview Content */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 9DE15EF095B4D72E2995B4D8 /* Preview Assets.xcassets */, 58 | ); 59 | path = "Preview Content"; 60 | sourceTree = ""; 61 | }; 62 | 7AA888B4D9326284E1A1C4D5 /* ScreenshotOrganizer */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | A4B3DB258197C6BBD4EE983D /* AppDelegate.swift */, 66 | 7AD13073E20B6F007EB27850 /* AppLogger.swift */, 67 | CAC25A121C87606D74433D2F /* Assets.xcassets */, 68 | 0959B6B8684EC41A4C1C1E4F /* Constants.swift */, 69 | 969CDD22B665D91F5AA9FC20 /* FileMonitor.swift */, 70 | 2856C72A01787D1B6859310B /* Info.plist */, 71 | 8C9F70F9EFB75F4672B49578 /* KeyboardShortcutNames.swift */, 72 | A8318E21DF06EFAC8B995CBA /* main.swift */, 73 | C36F5CBF7F34250B6EC66D6C /* ScreenshotOrganizer.entitlements */, 74 | 683E1C0A1A5B2422E2044FD0 /* SettingsContentView.swift */, 75 | 39F536E9DE81EF8286DB1B07 /* Preview Content */, 76 | ); 77 | path = ScreenshotOrganizer; 78 | sourceTree = ""; 79 | }; 80 | 9A94D6F7424FCA564B4350DC /* Products */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | 3D9318DA5CC071CF1DA9AD6B /* ScreenshotOrganizer.app */, 84 | 470F50A7532E508B244A2F3C /* ScreenshotOrganizerLauncher.app */, 85 | ); 86 | name = Products; 87 | sourceTree = ""; 88 | }; 89 | DF814444297AF4D29E7698FE = { 90 | isa = PBXGroup; 91 | children = ( 92 | 7AA888B4D9326284E1A1C4D5 /* ScreenshotOrganizer */, 93 | E40403990A55151DEBB01E3C /* ScreenshotOrganizerLauncher */, 94 | 9A94D6F7424FCA564B4350DC /* Products */, 95 | ); 96 | sourceTree = ""; 97 | }; 98 | E40403990A55151DEBB01E3C /* ScreenshotOrganizerLauncher */ = { 99 | isa = PBXGroup; 100 | children = ( 101 | D7105D43346BA15BCA978F3E /* Info.plist */, 102 | C0F1011AB6F8387A658546FA /* main.swift */, 103 | 2B0729CC69F3D53BF8941E9C /* ScreenshotOrganizerLauncher.entitlements */, 104 | ); 105 | path = ScreenshotOrganizerLauncher; 106 | sourceTree = ""; 107 | }; 108 | /* End PBXGroup section */ 109 | 110 | /* Begin PBXNativeTarget section */ 111 | A6B28E7EE7720DD0B412AFC9 /* ScreenshotOrganizerLauncher */ = { 112 | isa = PBXNativeTarget; 113 | buildConfigurationList = 1648E22303E4B09BC91B0CFB /* Build configuration list for PBXNativeTarget "ScreenshotOrganizerLauncher" */; 114 | buildPhases = ( 115 | 72E80859853AA041F4DC7290 /* Sources */, 116 | ); 117 | buildRules = ( 118 | ); 119 | dependencies = ( 120 | ); 121 | name = ScreenshotOrganizerLauncher; 122 | productName = ScreenshotOrganizerLauncher; 123 | productReference = 470F50A7532E508B244A2F3C /* ScreenshotOrganizerLauncher.app */; 124 | productType = "com.apple.product-type.application"; 125 | }; 126 | AB0B5B9B7DD6C2AD558F4565 /* ScreenshotOrganizer */ = { 127 | isa = PBXNativeTarget; 128 | buildConfigurationList = 5FE1330EF1EC8E53BA490947 /* Build configuration list for PBXNativeTarget "ScreenshotOrganizer" */; 129 | buildPhases = ( 130 | BB4CEFF5A5E431E5C4DBDACD /* Sources */, 131 | 587927A3403F389F7524A2B9 /* Resources */, 132 | A032DA31A0F7B0882268EF2B /* Frameworks */, 133 | ); 134 | buildRules = ( 135 | ); 136 | dependencies = ( 137 | ); 138 | name = ScreenshotOrganizer; 139 | packageProductDependencies = ( 140 | 50A1B56541822A866684DEEB /* KeyboardShortcuts */, 141 | ); 142 | productName = ScreenshotOrganizer; 143 | productReference = 3D9318DA5CC071CF1DA9AD6B /* ScreenshotOrganizer.app */; 144 | productType = "com.apple.product-type.application"; 145 | }; 146 | /* End PBXNativeTarget section */ 147 | 148 | /* Begin PBXProject section */ 149 | 4C98DE92F05401A3ED94CC04 /* Project object */ = { 150 | isa = PBXProject; 151 | attributes = { 152 | BuildIndependentTargetsInParallel = YES; 153 | LastUpgradeCheck = 1430; 154 | TargetAttributes = { 155 | }; 156 | }; 157 | buildConfigurationList = AC247587F94424D7E6AABF26 /* Build configuration list for PBXProject "ScreenshotOrganizer" */; 158 | compatibilityVersion = "Xcode 14.0"; 159 | developmentRegion = en; 160 | hasScannedForEncodings = 0; 161 | knownRegions = ( 162 | Base, 163 | en, 164 | ); 165 | mainGroup = DF814444297AF4D29E7698FE; 166 | packageReferences = ( 167 | FB29E7869B23DFC89A40ACD7 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */, 168 | ); 169 | projectDirPath = ""; 170 | projectRoot = ""; 171 | targets = ( 172 | AB0B5B9B7DD6C2AD558F4565 /* ScreenshotOrganizer */, 173 | A6B28E7EE7720DD0B412AFC9 /* ScreenshotOrganizerLauncher */, 174 | ); 175 | }; 176 | /* End PBXProject section */ 177 | 178 | /* Begin PBXResourcesBuildPhase section */ 179 | 587927A3403F389F7524A2B9 /* Resources */ = { 180 | isa = PBXResourcesBuildPhase; 181 | buildActionMask = 2147483647; 182 | files = ( 183 | 2D412C08CB31202D939B3AE5 /* Assets.xcassets in Resources */, 184 | A59220CD130A1070D2C0A8FD /* Preview Assets.xcassets in Resources */, 185 | ); 186 | runOnlyForDeploymentPostprocessing = 0; 187 | }; 188 | /* End PBXResourcesBuildPhase section */ 189 | 190 | /* Begin PBXSourcesBuildPhase section */ 191 | 72E80859853AA041F4DC7290 /* Sources */ = { 192 | isa = PBXSourcesBuildPhase; 193 | buildActionMask = 2147483647; 194 | files = ( 195 | 4ADDA6D17B56CFA588B5A757 /* main.swift in Sources */, 196 | ); 197 | runOnlyForDeploymentPostprocessing = 0; 198 | }; 199 | BB4CEFF5A5E431E5C4DBDACD /* Sources */ = { 200 | isa = PBXSourcesBuildPhase; 201 | buildActionMask = 2147483647; 202 | files = ( 203 | 475CD581BFB94856E6344294 /* AppDelegate.swift in Sources */, 204 | 7BBBAD5AC2F29166995B6D85 /* AppLogger.swift in Sources */, 205 | 54508DE0D293274A8807655C /* Constants.swift in Sources */, 206 | F3053729E386DF10D38913A5 /* FileMonitor.swift in Sources */, 207 | F9DE0C064B87FD976AE381C9 /* KeyboardShortcutNames.swift in Sources */, 208 | AEF9EC7B1F82BCE5C556F458 /* SettingsContentView.swift in Sources */, 209 | DA49A14B6FB9F2B487241EFD /* main.swift in Sources */, 210 | ); 211 | runOnlyForDeploymentPostprocessing = 0; 212 | }; 213 | /* End PBXSourcesBuildPhase section */ 214 | 215 | /* Begin XCBuildConfiguration section */ 216 | 7D261FC8A53A7DA9E4923801 /* Debug */ = { 217 | isa = XCBuildConfiguration; 218 | buildSettings = { 219 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 220 | CODE_SIGN_ENTITLEMENTS = ScreenshotOrganizer/ScreenshotOrganizer.entitlements; 221 | COMBINE_HIDPI_IMAGES = YES; 222 | INFOPLIST_FILE = ScreenshotOrganizer/Info.plist; 223 | LD_RUNPATH_SEARCH_PATHS = ( 224 | "$(inherited)", 225 | "@executable_path/../Frameworks", 226 | ); 227 | PRODUCT_BUNDLE_IDENTIFIER = com.reorx.ScreenshotOrganizer; 228 | SDKROOT = macosx; 229 | }; 230 | name = Debug; 231 | }; 232 | 7E0D6BE70184975CBF9AEEE6 /* Debug */ = { 233 | isa = XCBuildConfiguration; 234 | buildSettings = { 235 | ALWAYS_SEARCH_USER_PATHS = NO; 236 | CLANG_ANALYZER_NONNULL = YES; 237 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 238 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 239 | CLANG_CXX_LIBRARY = "libc++"; 240 | CLANG_ENABLE_MODULES = YES; 241 | CLANG_ENABLE_OBJC_ARC = YES; 242 | CLANG_ENABLE_OBJC_WEAK = YES; 243 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 244 | CLANG_WARN_BOOL_CONVERSION = YES; 245 | CLANG_WARN_COMMA = YES; 246 | CLANG_WARN_CONSTANT_CONVERSION = YES; 247 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 248 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 249 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 250 | CLANG_WARN_EMPTY_BODY = YES; 251 | CLANG_WARN_ENUM_CONVERSION = YES; 252 | CLANG_WARN_INFINITE_RECURSION = YES; 253 | CLANG_WARN_INT_CONVERSION = YES; 254 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 255 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 256 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 257 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 258 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 259 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 260 | CLANG_WARN_STRICT_PROTOTYPES = YES; 261 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 262 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 263 | CLANG_WARN_UNREACHABLE_CODE = YES; 264 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 265 | COPY_PHASE_STRIP = NO; 266 | DEBUG_INFORMATION_FORMAT = dwarf; 267 | ENABLE_STRICT_OBJC_MSGSEND = YES; 268 | ENABLE_TESTABILITY = YES; 269 | GCC_C_LANGUAGE_STANDARD = gnu11; 270 | GCC_DYNAMIC_NO_PIC = NO; 271 | GCC_NO_COMMON_BLOCKS = YES; 272 | GCC_OPTIMIZATION_LEVEL = 0; 273 | GCC_PREPROCESSOR_DEFINITIONS = ( 274 | "$(inherited)", 275 | "DEBUG=1", 276 | ); 277 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 278 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 279 | GCC_WARN_UNDECLARED_SELECTOR = YES; 280 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 281 | GCC_WARN_UNUSED_FUNCTION = YES; 282 | GCC_WARN_UNUSED_VARIABLE = YES; 283 | MACOSX_DEPLOYMENT_TARGET = 12.0; 284 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 285 | MTL_FAST_MATH = YES; 286 | ONLY_ACTIVE_ARCH = YES; 287 | PRODUCT_NAME = "$(TARGET_NAME)"; 288 | SDKROOT = macosx; 289 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 290 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 291 | SWIFT_VERSION = 5.0; 292 | }; 293 | name = Debug; 294 | }; 295 | 8EFEB311A04AFB4A6DA118BC /* Debug */ = { 296 | isa = XCBuildConfiguration; 297 | buildSettings = { 298 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 299 | CODE_SIGN_ENTITLEMENTS = ScreenshotOrganizerLauncher/ScreenshotOrganizerLauncher.entitlements; 300 | COMBINE_HIDPI_IMAGES = YES; 301 | INFOPLIST_FILE = ScreenshotOrganizerLauncher/Info.plist; 302 | LD_RUNPATH_SEARCH_PATHS = ( 303 | "$(inherited)", 304 | "@executable_path/../Frameworks", 305 | ); 306 | PRODUCT_BUNDLE_IDENTIFIER = com.reorx.ScreenshotOrganizer.Launcher; 307 | SDKROOT = macosx; 308 | }; 309 | name = Debug; 310 | }; 311 | 92871FE538B3F11F58104E97 /* Release */ = { 312 | isa = XCBuildConfiguration; 313 | buildSettings = { 314 | ALWAYS_SEARCH_USER_PATHS = NO; 315 | CLANG_ANALYZER_NONNULL = YES; 316 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 317 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 318 | CLANG_CXX_LIBRARY = "libc++"; 319 | CLANG_ENABLE_MODULES = YES; 320 | CLANG_ENABLE_OBJC_ARC = YES; 321 | CLANG_ENABLE_OBJC_WEAK = YES; 322 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 323 | CLANG_WARN_BOOL_CONVERSION = YES; 324 | CLANG_WARN_COMMA = YES; 325 | CLANG_WARN_CONSTANT_CONVERSION = YES; 326 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 327 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 328 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 329 | CLANG_WARN_EMPTY_BODY = YES; 330 | CLANG_WARN_ENUM_CONVERSION = YES; 331 | CLANG_WARN_INFINITE_RECURSION = YES; 332 | CLANG_WARN_INT_CONVERSION = YES; 333 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 334 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 335 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 336 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 337 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 338 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 339 | CLANG_WARN_STRICT_PROTOTYPES = YES; 340 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 341 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 342 | CLANG_WARN_UNREACHABLE_CODE = YES; 343 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 344 | COPY_PHASE_STRIP = NO; 345 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 346 | ENABLE_NS_ASSERTIONS = NO; 347 | ENABLE_STRICT_OBJC_MSGSEND = YES; 348 | GCC_C_LANGUAGE_STANDARD = gnu11; 349 | GCC_NO_COMMON_BLOCKS = YES; 350 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 351 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 352 | GCC_WARN_UNDECLARED_SELECTOR = YES; 353 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 354 | GCC_WARN_UNUSED_FUNCTION = YES; 355 | GCC_WARN_UNUSED_VARIABLE = YES; 356 | MACOSX_DEPLOYMENT_TARGET = 12.0; 357 | MTL_ENABLE_DEBUG_INFO = NO; 358 | MTL_FAST_MATH = YES; 359 | PRODUCT_NAME = "$(TARGET_NAME)"; 360 | SDKROOT = macosx; 361 | SWIFT_COMPILATION_MODE = wholemodule; 362 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 363 | SWIFT_VERSION = 5.0; 364 | }; 365 | name = Release; 366 | }; 367 | A11526FEEB5950E07E823DF7 /* Release */ = { 368 | isa = XCBuildConfiguration; 369 | buildSettings = { 370 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 371 | CODE_SIGN_ENTITLEMENTS = ScreenshotOrganizer/ScreenshotOrganizer.entitlements; 372 | COMBINE_HIDPI_IMAGES = YES; 373 | INFOPLIST_FILE = ScreenshotOrganizer/Info.plist; 374 | LD_RUNPATH_SEARCH_PATHS = ( 375 | "$(inherited)", 376 | "@executable_path/../Frameworks", 377 | ); 378 | PRODUCT_BUNDLE_IDENTIFIER = com.reorx.ScreenshotOrganizer; 379 | SDKROOT = macosx; 380 | }; 381 | name = Release; 382 | }; 383 | D45BA8330B984650081930C5 /* Release */ = { 384 | isa = XCBuildConfiguration; 385 | buildSettings = { 386 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 387 | CODE_SIGN_ENTITLEMENTS = ScreenshotOrganizerLauncher/ScreenshotOrganizerLauncher.entitlements; 388 | COMBINE_HIDPI_IMAGES = YES; 389 | INFOPLIST_FILE = ScreenshotOrganizerLauncher/Info.plist; 390 | LD_RUNPATH_SEARCH_PATHS = ( 391 | "$(inherited)", 392 | "@executable_path/../Frameworks", 393 | ); 394 | PRODUCT_BUNDLE_IDENTIFIER = com.reorx.ScreenshotOrganizer.Launcher; 395 | SDKROOT = macosx; 396 | }; 397 | name = Release; 398 | }; 399 | /* End XCBuildConfiguration section */ 400 | 401 | /* Begin XCConfigurationList section */ 402 | 1648E22303E4B09BC91B0CFB /* Build configuration list for PBXNativeTarget "ScreenshotOrganizerLauncher" */ = { 403 | isa = XCConfigurationList; 404 | buildConfigurations = ( 405 | 8EFEB311A04AFB4A6DA118BC /* Debug */, 406 | D45BA8330B984650081930C5 /* Release */, 407 | ); 408 | defaultConfigurationIsVisible = 0; 409 | defaultConfigurationName = Debug; 410 | }; 411 | 5FE1330EF1EC8E53BA490947 /* Build configuration list for PBXNativeTarget "ScreenshotOrganizer" */ = { 412 | isa = XCConfigurationList; 413 | buildConfigurations = ( 414 | 7D261FC8A53A7DA9E4923801 /* Debug */, 415 | A11526FEEB5950E07E823DF7 /* Release */, 416 | ); 417 | defaultConfigurationIsVisible = 0; 418 | defaultConfigurationName = Debug; 419 | }; 420 | AC247587F94424D7E6AABF26 /* Build configuration list for PBXProject "ScreenshotOrganizer" */ = { 421 | isa = XCConfigurationList; 422 | buildConfigurations = ( 423 | 7E0D6BE70184975CBF9AEEE6 /* Debug */, 424 | 92871FE538B3F11F58104E97 /* Release */, 425 | ); 426 | defaultConfigurationIsVisible = 0; 427 | defaultConfigurationName = Debug; 428 | }; 429 | /* End XCConfigurationList section */ 430 | 431 | /* Begin XCRemoteSwiftPackageReference section */ 432 | FB29E7869B23DFC89A40ACD7 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */ = { 433 | isa = XCRemoteSwiftPackageReference; 434 | repositoryURL = "https://github.com/sindresorhus/KeyboardShortcuts"; 435 | requirement = { 436 | kind = upToNextMajorVersion; 437 | minimumVersion = 1.0.0; 438 | }; 439 | }; 440 | /* End XCRemoteSwiftPackageReference section */ 441 | 442 | /* Begin XCSwiftPackageProductDependency section */ 443 | 50A1B56541822A866684DEEB /* KeyboardShortcuts */ = { 444 | isa = XCSwiftPackageProductDependency; 445 | package = FB29E7869B23DFC89A40ACD7 /* XCRemoteSwiftPackageReference "KeyboardShortcuts" */; 446 | productName = KeyboardShortcuts; 447 | }; 448 | /* End XCSwiftPackageProductDependency section */ 449 | }; 450 | rootObject = 4C98DE92F05401A3ED94CC04 /* Project object */; 451 | } 452 | --------------------------------------------------------------------------------