├── .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 |
--------------------------------------------------------------------------------