Blink: allow viewing and editing captions
- ID
ee6f05d- date
2026-07-03 21:07:45+00:00- author
Alex Chan <alex@alexwlchan.net>- parent
584f492- message
Blink: allow viewing and editing captions This adds a new CaptionOverlay view on the currently focused image, which shows the current caption. Tapping space switches to a text field where I can edit or enter a new caption, which is then saved to the Photos database.- changed files
7 files, 305 additions, 31 deletions
Changed files
Blink.xcodeproj/project.pbxproj (35857) → Blink.xcodeproj/project.pbxproj (36689)
diff --git a/Blink.xcodeproj/project.pbxproj b/Blink.xcodeproj/project.pbxproj
index b306942..7c184d8 100644
--- a/Blink.xcodeproj/project.pbxproj
+++ b/Blink.xcodeproj/project.pbxproj
@@ -10,6 +10,7 @@
940331732A336B5100200C5D /* DeferredRendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 940331722A336B5100200C5D /* DeferredRendering.swift */; };
9404B57D2A3EEBBA00068FA8 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9404B57C2A3EEBBA00068FA8 /* SettingsView.swift */; };
941E18FA2A35362600A2EA98 /* Info.swift in Sources */ = {isa = PBXBuildFile; fileRef = 941E18F92A35362600A2EA98 /* Info.swift */; };
+ 944EB0DB2FF1A0D5002C81C3 /* CaptionOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 944EB0DA2FF1A0D1002C81C3 /* CaptionOverlay.swift */; };
944EB0DF2FF1AF01002C81C3 /* ScriptHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 944EB0DE2FF1AEFE002C81C3 /* ScriptHelper.swift */; };
945F17B02A33D167004FC479 /* ThumbnailImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945F17AF2A33D167004FC479 /* ThumbnailImage.swift */; };
945F17B22A33D69B004FC479 /* FavoriteOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945F17B12A33D69B004FC479 /* FavoriteOverlay.swift */; };
@@ -62,6 +63,7 @@
940331722A336B5100200C5D /* DeferredRendering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredRendering.swift; sourceTree = "<group>"; };
9404B57C2A3EEBBA00068FA8 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
941E18F92A35362600A2EA98 /* Info.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Info.swift; sourceTree = "<group>"; };
+ 944EB0DA2FF1A0D1002C81C3 /* CaptionOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptionOverlay.swift; sourceTree = "<group>"; };
944EB0DE2FF1AEFE002C81C3 /* ScriptHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptHelper.swift; sourceTree = "<group>"; };
945F17AF2A33D167004FC479 /* ThumbnailImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThumbnailImage.swift; sourceTree = "<group>"; };
945F17B12A33D69B004FC479 /* FavoriteOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteOverlay.swift; sourceTree = "<group>"; };
@@ -156,6 +158,7 @@
94A0835F2A33E7E900238964 /* FocusedImage */ = {
isa = PBXGroup;
children = (
+ 944EB0DA2FF1A0D1002C81C3 /* CaptionOverlay.swift */,
94A0835D2A33E49E00238964 /* FocusedImage.swift */,
94A083602A33E98000238964 /* AlbumInfoOverlay.swift */,
94A083622A33F30300238964 /* LoadingIndicatorOverlay.swift */,
@@ -335,7 +338,7 @@
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1430;
- LastUpgradeCheck = 1430;
+ LastUpgradeCheck = 2620;
TargetAttributes = {
94D750EB2A31A796005859E7 = {
CreatedOnToolsVersion = 14.3.1;
@@ -437,6 +440,7 @@
94A083612A33E98000238964 /* AlbumInfoOverlay.swift in Sources */,
94C5FFF22A33ADD4004ADDF5 /* PHFetchResultCollection.swift in Sources */,
944EB0DF2FF1AF01002C81C3 /* ScriptHelper.swift in Sources */,
+ 944EB0DB2FF1A0D5002C81C3 /* CaptionOverlay.swift in Sources */,
94A083662A33F50900238964 /* Debug.swift in Sources */,
94A083682A33F6E900238964 /* ThumbnailList.swift in Sources */,
94D750F02A31A796005859E7 /* BlinkApp.swift in Sources */,
@@ -520,6 +524,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -542,6 +547,7 @@
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
@@ -580,6 +586,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
+ DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -595,6 +602,7 @@
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
+ STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
};
@@ -605,13 +613,16 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ AUTOMATION_APPLE_EVENTS = YES;
CODE_SIGN_ENTITLEMENTS = Blink/Blink.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 136;
+ CURRENT_PROJECT_VERSION = 199;
+ DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Blink/Preview Content\"";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
+ ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography";
INFOPLIST_KEY_NSHumanReadableCopyright = "Made by Alex Chan <alex@alexwlchan.net>";
@@ -620,6 +631,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
+ MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = net.alexwlchan.Blink;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -634,13 +646,16 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ AUTOMATION_APPLE_EVENTS = YES;
CODE_SIGN_ENTITLEMENTS = Blink/Blink.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
- CURRENT_PROJECT_VERSION = 136;
+ CURRENT_PROJECT_VERSION = 199;
+ DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_ASSET_PATHS = "\"Blink/Preview Content\"";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
+ ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography";
INFOPLIST_KEY_NSHumanReadableCopyright = "Made by Alex Chan <alex@alexwlchan.net>";
@@ -649,6 +664,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
+ MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = net.alexwlchan.Blink;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -661,10 +677,10 @@
94D751152A31A798005859E7 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 136;
+ CURRENT_PROJECT_VERSION = 199;
+ DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 13.3;
MARKETING_VERSION = 1.0;
@@ -679,10 +695,10 @@
94D751162A31A798005859E7 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 136;
+ CURRENT_PROJECT_VERSION = 199;
+ DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 13.3;
MARKETING_VERSION = 1.0;
@@ -697,9 +713,9 @@
94D751182A31A798005859E7 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 136;
+ CURRENT_PROJECT_VERSION = 199;
+ DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = net.alexwlchan.BlinkReviewerUITests;
@@ -713,9 +729,9 @@
94D751192A31A798005859E7 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
- ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 136;
+ CURRENT_PROJECT_VERSION = 199;
+ DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = net.alexwlchan.BlinkReviewerUITests;
Blink.xcodeproj/xcshareddata/xcschemes/Blink.xcscheme (2824) → Blink.xcodeproj/xcshareddata/xcschemes/Blink.xcscheme (2824)
diff --git a/Blink.xcodeproj/xcshareddata/xcschemes/Blink.xcscheme b/Blink.xcodeproj/xcshareddata/xcschemes/Blink.xcscheme
index 23503ec..bba7fb3 100644
--- a/Blink.xcodeproj/xcshareddata/xcschemes/Blink.xcscheme
+++ b/Blink.xcodeproj/xcshareddata/xcschemes/Blink.xcscheme
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
- LastUpgradeVersion = "1430"
+ LastUpgradeVersion = "2620"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
Blink/Helpers/ScriptHelper.swift (976) → Blink/Helpers/ScriptHelper.swift (1906)
diff --git a/Blink/Helpers/ScriptHelper.swift b/Blink/Helpers/ScriptHelper.swift
index 3e45e4a..37afce2 100644
--- a/Blink/Helpers/ScriptHelper.swift
+++ b/Blink/Helpers/ScriptHelper.swift
@@ -1,29 +1,48 @@
import Foundation
/// ScriptHelper is a helper for invoking AppleScript.
-///
-/// This allows
enum ScriptHelper {
/// Execute an AppleScript via osascript with the provided arguments.
/// Returns the output or throws an error.
@discardableResult
- static func runAppleScript(_ script: String, arguments: [String]) throws -> String {
- let task = Process()
- task.launchPath = "/usr/bin/osascript"
-
- var args = ["-e", script]
- args.append(contentsOf: arguments)
- task.arguments = args
-
- let pipe = Pipe()
- task.standardOutput = pipe
- task.standardError = Pipe()
-
- try task.run()
- task.waitUntilExit()
-
- let data = pipe.fileHandleForReading.readDataToEndOfFile()
+ static func runAppleScript(_ script: String, arguments: [String]) async throws -> String {
+ try await Task.detached(priority: .userInitiated) {
+ let task = Process()
+ task.launchPath = "/usr/bin/osascript"
+
+ var args = ["-e", script]
+ args.append(contentsOf: arguments)
+ task.arguments = args
+
+ let stdoutPipe = Pipe()
+ let stderrPipe = Pipe()
+ task.standardOutput = stdoutPipe
+ task.standardError = stderrPipe
+
+ try task.run()
+ task.waitUntilExit()
+
+ let stdout = self.readAsString(stdoutPipe)
+ let stderr = self.readAsString(stderrPipe)
+
+ print("AppleScript output: status=\(task.terminationStatus), stdout=\(stdout), stderr=\(stderr)")
+
+ if task.terminationStatus != 0 {
+ let errorMessage = stderr.isEmpty ? "Unknown AppleScript execution error." : "AppleScript execution error: \(stderr)"
+ throw NSError(
+ domain: "ScriptHelperDomain",
+ code: Int(task.terminationStatus),
+ userInfo: [NSLocalizedDescriptionKey: errorMessage]
+ )
+ }
+
+ return stdout
+ }.value
+ }
+
+ private static func readAsString(_ p: Pipe) -> String {
+ let data = p.fileHandleForReading.readDataToEndOfFile()
guard let output = String(data: data, encoding: .utf8) else {
return ""
}
Blink/Photos/AssetHelpers.swift (2485) → Blink/Photos/AssetHelpers.swift (3953)
diff --git a/Blink/Photos/AssetHelpers.swift b/Blink/Photos/AssetHelpers.swift
index 6c7eb49..e8bedb4 100644
--- a/Blink/Photos/AssetHelpers.swift
+++ b/Blink/Photos/AssetHelpers.swift
@@ -74,4 +74,50 @@ extension PHAsset {
changeAlbum.addAssets(assets)
}
}
+
+ /// Fetch the caption from the Photos app.
+ func getCaption() async -> String {
+ let script = """
+ on run argv
+ set targetId to item 1 of argv
+
+ tell application "Photos"
+ set targetMedia to media item id targetId
+ set desc to description of targetMedia
+
+ if desc is missing value then
+ return ""
+ else
+ return desc
+ end if
+ end tell
+ end run
+ """
+ do {
+ return try await ScriptHelper.runAppleScript(script, arguments: [self.localIdentifier])
+ } catch {
+ print("Failed to run AppleScript getCaption: \(error)")
+ return ""
+ }
+ }
+
+ /// Update the caption in the Photos app.
+ func setCaption(_ caption: String) async {
+ let script = """
+ on run argv
+ set targetId to item 1 of argv
+ set newCaption to item 2 of argv
+
+ tell application "Photos"
+ set targetMedia to media item id targetId
+ set description of targetMedia to newCaption
+ end tell
+ end run
+ """
+ do {
+ try await ScriptHelper.runAppleScript(script, arguments: [self.localIdentifier, caption])
+ } catch {
+ print("Failed to run AppleScript setCaption: \(error)")
+ }
+ }
}
Blink/Views/FocusedImage/CaptionOverlay.swift (0) → Blink/Views/FocusedImage/CaptionOverlay.swift (5829)
diff --git a/Blink/Views/FocusedImage/CaptionOverlay.swift b/Blink/Views/FocusedImage/CaptionOverlay.swift
index e69de29..4f388b5 100644
--- a/Blink/Views/FocusedImage/CaptionOverlay.swift
+++ b/Blink/Views/FocusedImage/CaptionOverlay.swift
@@ -0,0 +1,167 @@
+import SwiftUI
+import Photos
+
+/// Render the current caption on top of the image.
+struct CaptionOverlay: ViewModifier {
+ var asset: PHAsset
+
+ @State private var captionText: String = ""
+ @State private var isEditing: Bool = false
+ @State private var isLoading: Bool = false
+
+ // Holds a task that delays the "caption loading" spinner so we don't get
+ // short-lived flashes of loading content.
+ @State private var spinnerDelayTask: Task<Void, Never>? = nil
+
+ init(asset: PHAsset) {
+ self.asset = asset
+ }
+
+ func body(content: Content) -> some View {
+ content.overlay(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
+ HStack {
+ if isLoading && captionText.isEmpty {
+ ProgressView().controlSize(.small)
+ } else if isEditing {
+ CaptionEditorView(
+ text: $captionText,
+ onSave: saveAndExit,
+ onCancel: revertAndExit
+ )
+ } else {
+ Text(self.captionText.isEmpty ? "Add a caption..." : captionText)
+ .font(.headline)
+ .italic(captionText.isEmpty)
+
+ .onTapGesture {
+ enterEditingMode()
+ }
+ .padding()
+ }
+ }
+ .foregroundColor(captionText.isEmpty ? .gray : .white)
+ .frame(maxWidth: .infinity, minHeight: 44, alignment: .center)
+ .background(Color.black.opacity(0.6))
+ .cornerRadius(8)
+ .padding()
+ // If the user presses the space key and we're not already editing
+ // or loading the caption, switch into the editing view.
+ .onReceive(NotificationCenter.default.publisher(for: .triggerCaptionEditing)) { _ in
+ if !isEditing && !isLoading {
+ enterEditingMode()
+ }
+ }
+ .task {
+ await fetchCaption()
+ }
+ }
+ }
+
+ /// fetchCaption fetches the caption text asynchronously, so it doesn't
+ /// block the main thread.
+ ///
+ /// We show a progress spinner while loading the caption, but only if it
+ /// takes more than a quarter of a second to load. In practice, captions
+ /// load near-instantly and so you normally don't see the spinnner.
+ private func fetchCaption() async {
+ spinnerDelayTask?.cancel()
+
+ spinnerDelayTask = Task {
+ try? await Task.sleep(for: .seconds(0.25))
+ if !Task.isCancelled {
+ isLoading = true
+ }
+ }
+
+ captionText = await asset.getCaption()
+
+ spinnerDelayTask?.cancel()
+ isLoading = false
+ }
+
+ /// enterEditingMode enables the text field, so the user can enter a caption.
+ private func enterEditingMode() {
+ isEditing = true
+ }
+
+ /// saveAndExit saves the new caption to the Photos app and closes the text field,
+ /// displaying the new caption.
+ private func saveAndExit() {
+ let textToSave = captionText
+ isEditing = false
+
+ Task {
+ await asset.setCaption(textToSave)
+ }
+ }
+
+ /// revertAndExit closes the text field and cancels the caption edit, reloading the
+ /// text from Photos app.
+ private func revertAndExit() {
+ isEditing = false
+
+ Task {
+ captionText = await asset.getCaption()
+ }
+ }
+}
+
+/// CaptionEditorView provides a text field for editing the caption.
+///
+/// There are "Cancel" and "Save" buttons on the right-hand side, which can
+/// be triggered with keyboard shortcuts "Esc" and "⌘+Enter".
+struct CaptionEditorView: View {
+ @Binding var text: String
+ var onSave: () -> Void
+ var onCancel: () -> Void
+
+ @FocusState private var isFocused: Bool
+
+ var body: some View {
+ HStack {
+ // Left-hand side: two invisible buttons offset the size of the
+ // buttons on the right-hand side, so text is centred in the window.
+ Button("Cancel") {} .hidden().allowsHitTesting(false).padding(.leading)
+ Button("Save") {}.hidden().allowsHitTesting(false)
+
+ // Centre: the editable text field.
+ TextField("Enter caption...", text: $text, axis: .vertical)
+ .textFieldStyle(.plain)
+ .foregroundColor(.white)
+ .multilineTextAlignment(.center)
+ .padding()
+ .focused($isFocused)
+ .onSubmit {
+ onSave()
+ }
+
+ // Right-hand side: buttons to cancel or save the current caption.
+ Button("Cancel") { onCancel() }
+ .buttonStyle(.bordered)
+ .keyboardShortcut(.escape, modifiers: [])
+
+ Button("Save") { onSave() }
+ .buttonStyle(.borderedProminent)
+ .keyboardShortcut(.return, modifiers: .command)
+ .padding(.trailing)
+ }
+ .onAppear {
+ // Automatically focus the text field as soon as this
+ // view is displayed.
+ isFocused = true
+ }
+ }
+}
+
+extension Notification.Name {
+ // triggerCaptionEditing is a notification sent by the keyDown handler
+ // in the top-level view when it receives a space, telling the overlay
+ // it should switch into editing mode.
+ static let triggerCaptionEditing = Notification.Name("triggerCaptionEditing")
+}
+
+extension View {
+ func caption(for asset: PHAsset) -> some View {
+ modifier(CaptionOverlay(asset: asset))
+ }
+}
Blink/Views/FocusedImage/FocusedImage.swift (2076) → Blink/Views/FocusedImage/FocusedImage.swift (2109)
diff --git a/Blink/Views/FocusedImage/FocusedImage.swift b/Blink/Views/FocusedImage/FocusedImage.swift
index 1e250f2..db1ee7d 100644
--- a/Blink/Views/FocusedImage/FocusedImage.swift
+++ b/Blink/Views/FocusedImage/FocusedImage.swift
@@ -42,6 +42,7 @@ private struct FocusedImageContent: View {
.draggable(Image(nsImage: assetImage.image))
.aspectRatio(contentMode: .fit)
.albumInfo(for: asset)
+ .caption(for: asset)
.loadingIndicator(isLoading: assetImage.isDegraded)
.contextMenu {
Button {
Blink/Views/PhotoReviewer.swift (14247) → Blink/Views/PhotoReviewer.swift (15313)
diff --git a/Blink/Views/PhotoReviewer.swift b/Blink/Views/PhotoReviewer.swift
index 77bf5bc..b6dd56b 100644
--- a/Blink/Views/PhotoReviewer.swift
+++ b/Blink/Views/PhotoReviewer.swift
@@ -214,6 +214,15 @@ struct PhotoReviewer: View {
/// issues, this results in an annoying "funk" sound playing on
/// every event, because the OS thinks the event is unhandled.
private func handleKeyDown(_ event: NSEvent) -> NSEvent? {
+
+ // Check if a text field is currently focused (in particular, the caption
+ // overlay) -- if so, let the user type directly into that text field
+ // rather than taking actions on the photo.
+ if let firstResponder = NSApp.keyWindow?.firstResponder,
+ firstResponder is NSTextView || firstResponder is NSTextField {
+ return event
+ }
+
let logger = Logger()
switch event {
@@ -328,7 +337,23 @@ struct PhotoReviewer: View {
end tell
end run
"""
- try! ScriptHelper.runAppleScript(script, arguments: [focusedAsset.localIdentifier])
+
+ let targetId = focusedAsset.localIdentifier
+
+ Task {
+ do {
+ try await ScriptHelper.runAppleScript(script, arguments: [targetId])
+ } catch {
+ logger.error("Failed to open asset in Photos: \(error.localizedDescription)")
+ }
+ }
+
+ return nil
+
+ // If the user types a space, send a notification to the CaptionOverlay view
+ // telling it to open the editing view (if it's not already open).
+ case let e where e.characters == " ":
+ NotificationCenter.default.post(name: .triggerCaptionEditing, object: nil)
return nil
default: