Skip to main content

allow marking a photo as reviewed

ID
57b22cc
date
2023-06-08 21:08:07+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
f1759e5
message
allow marking a photo as reviewed
changed files
4 files, 132 additions

Changed files

BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj (25747) → BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj (26189)

diff --git a/BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj b/BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj
index ea90af5..835d563 100644
--- a/BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj
+++ b/BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj
@@ -8,6 +8,7 @@
 
 /* Begin PBXBuildFile section */
 		94D2C8B92A320E6F00BEE15B /* ReviewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D2C8B82A320E6F00BEE15B /* ReviewState.swift */; };
+		94D2C8BD2A32796500BEE15B /* AlbumHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D2C8BC2A32796500BEE15B /* AlbumHelpers.swift */; };
 		94D750F02A31A796005859E7 /* BlinkReviewerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D750EF2A31A796005859E7 /* BlinkReviewerApp.swift */; };
 		94D750F22A31A796005859E7 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D750F12A31A796005859E7 /* ContentView.swift */; };
 		94D750F42A31A797005859E7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 94D750F32A31A797005859E7 /* Assets.xcassets */; };
@@ -42,6 +43,7 @@
 
 /* Begin PBXFileReference section */
 		94D2C8B82A320E6F00BEE15B /* ReviewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewState.swift; sourceTree = "<group>"; };
+		94D2C8BC2A32796500BEE15B /* AlbumHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumHelpers.swift; sourceTree = "<group>"; };
 		94D750EC2A31A796005859E7 /* BlinkReviewer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BlinkReviewer.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		94D750EF2A31A796005859E7 /* BlinkReviewerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlinkReviewerApp.swift; sourceTree = "<group>"; };
 		94D750F12A31A796005859E7 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -170,6 +172,7 @@
 			isa = PBXGroup;
 			children = (
 				94D7512A2A31D6AC005859E7 /* AssetHelpers.swift */,
+				94D2C8BC2A32796500BEE15B /* AlbumHelpers.swift */,
 			);
 			path = Photos;
 			sourceTree = "<group>";
@@ -307,6 +310,7 @@
 				94D7511C2A31A7B1005859E7 /* ThumbnailImage.swift in Sources */,
 				94D750F22A31A796005859E7 /* ContentView.swift in Sources */,
 				94D7512B2A31D6AC005859E7 /* AssetHelpers.swift in Sources */,
+				94D2C8BD2A32796500BEE15B /* AlbumHelpers.swift in Sources */,
 				94D7511E2A31B243005859E7 /* PreviewImage.swift in Sources */,
 				94D750F02A31A796005859E7 /* BlinkReviewerApp.swift in Sources */,
 				94D751202A31B53E005859E7 /* AlbumInfo.swift in Sources */,

BlinkReviewer/BlinkReviewer/Photos/AlbumHelpers.swift (0) → BlinkReviewer/BlinkReviewer/Photos/AlbumHelpers.swift (807)

diff --git a/BlinkReviewer/BlinkReviewer/Photos/AlbumHelpers.swift b/BlinkReviewer/BlinkReviewer/Photos/AlbumHelpers.swift
new file mode 100644
index 0000000..2da334e
--- /dev/null
+++ b/BlinkReviewer/BlinkReviewer/Photos/AlbumHelpers.swift
@@ -0,0 +1,34 @@
+//
+//  AlbumHelpers.swift
+//  BlinkReviewer
+//
+//  Created by Alex Chan on 08/06/2023.
+//
+
+import Foundation
+import Photos
+
+/// Looks up an album by name.
+///
+/// This assumes that album names are globally unique.
+func getAlbum(withName name: String) -> PHAssetCollection {
+  let collections =
+    PHAssetCollection
+    .fetchAssetCollections(with: .album, subtype: .albumRegular, options: nil)
+
+  var thisAssetCollection: PHAssetCollection? = nil
+
+  collections.enumerateObjects({ (album, index, stop) in
+    let assetCollection = album
+
+    if assetCollection.localizedTitle == Optional(name) {
+      thisAssetCollection = assetCollection
+    }
+  })
+
+  if let assetCollection = thisAssetCollection {
+    return assetCollection
+  } else {
+    fatalError("Unable to find album with name: \(name).\n")
+  }
+}

BlinkReviewer/BlinkReviewer/Photos/AssetHelpers.swift (2934) → BlinkReviewer/BlinkReviewer/Photos/AssetHelpers.swift (4726)

diff --git a/BlinkReviewer/BlinkReviewer/Photos/AssetHelpers.swift b/BlinkReviewer/BlinkReviewer/Photos/AssetHelpers.swift
index 97722b5..bb5ea0b 100644
--- a/BlinkReviewer/BlinkReviewer/Photos/AssetHelpers.swift
+++ b/BlinkReviewer/BlinkReviewer/Photos/AssetHelpers.swift
@@ -98,4 +98,53 @@ extension PHAsset {
     func getImage() -> NSImage {
         return getImageForSize(size: PHImageManagerMaximumSize)
     }
+    
+    /// Returns true if an asset is in the given album, false otherwise.
+    func isInAlbum(_ album: PHAssetCollection) -> Bool {
+        return albums().contains(where: { collection in
+            collection == album
+        })
+    }
+    
+    /// Remove a photo from an album.
+    ///
+    /// This expects to be run inside a performChangesAndWait change block;
+    /// see https://developer.apple.com/documentation/photokit/phphotolibrary/1620747-performchangesandwait.
+    func remove(fromAlbum album: PHAssetCollection) -> Void {
+      let changeAlbum =
+        PHAssetCollectionChangeRequest(for: album)!
+
+      changeAlbum.removeAssets([self] as NSFastEnumeration)
+    }
+
+    /// Add a photo to an album.
+    ///
+    /// This expects to be run inside a performChangesAndWait change block;
+    /// see https://developer.apple.com/documentation/photokit/phphotolibrary/1620747-performchangesandwait.
+    func add(toAlbum album: PHAssetCollection) -> Void {
+      let changeAlbum =
+        PHAssetCollectionChangeRequest(for: album)!
+
+      changeAlbum.addAssets([self] as NSFastEnumeration)
+    }
+    
+    /// Toggle a photo's inclusion in an album.
+    ///
+    /// If the photo is already in the album, remove it.  If the photo isn't
+    /// in the album, add it.
+    ///
+    /// This expects to be run inside a performChangesAndWait change block;
+    /// see https://developer.apple.com/documentation/photokit/phphotolibrary/1620747-performchangesandwait.
+    func toggle(inAlbum album: PHAssetCollection) -> Void {
+      let changeAlbum =
+        PHAssetCollectionChangeRequest(for: album)!
+
+      let assets = [self] as NSFastEnumeration
+
+      if self.isInAlbum(album) {
+        changeAlbum.removeAssets(assets)
+      } else {
+        changeAlbum.addAssets(assets)
+      }
+    }
 }

BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift (1162) → BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift (3128)

diff --git a/BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift b/BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift
index c2517f2..db0287a 100644
--- a/BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift
+++ b/BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift
@@ -27,6 +27,8 @@ struct PhotoReviewer: View {
     }
     
     private func handleKeyEvent(_ event: NSEvent) {
+        let asset = assets[selectedAssetIndex]
+        
         switch event.keyCode {
             case 123: // Left arrow key
                 if selectedAssetIndex > 0 {
@@ -38,6 +40,49 @@ struct PhotoReviewer: View {
                     selectedAssetIndex += 1
                 }
             
+            case 18, 19, 20: // "1", "2", "3"
+                let approved = getAlbum(withName: "Approved")
+                let rejected = getAlbum(withName: "Rejected")
+                let needsAction = getAlbum(withName: "Needs Action")
+
+                let albums = asset.albums()
+
+                let isApproved = albums.contains(approved)
+                let isRejected = albums.contains(rejected)
+                let isNeedsAction = albums.contains(needsAction)
+            
+                try! PHPhotoLibrary.shared().performChangesAndWait {
+                    // Strictly speaking, the first condition is a combination of two:
+                    //
+                    //   1. The action is `toggle-approved` and the photo is approved,
+                    //      in which case toggling means un-approving it.
+                    //   2. The action is anything else and the photo is approved, in
+                    //      which case setting the new status means removing approved.
+                    //
+                    // Similar logic applies for all three conditions.
+                    if isApproved {
+                      asset.remove(fromAlbum: approved)
+                    } else if event.keyCode == 18 {
+                        asset.add(toAlbum: approved)
+                    }
+
+                    if isRejected {
+                        asset.remove(fromAlbum: rejected)
+                    } else if event.keyCode == 19 {
+                        asset.add(toAlbum: rejected)
+                    }
+
+                    if isNeedsAction {
+                        asset.remove(fromAlbum: needsAction)
+                    } else if event.keyCode == 20 {
+                        asset.add(toAlbum: needsAction)
+                    }
+                }
+            
+                if selectedAssetIndex > 0 {
+                    selectedAssetIndex -= 1
+                }
+            
             default:
                 print(event)
                 break