fiddle with album overlays, allow reviewing again
- ID
1d997c8- date
2023-06-09 23:30:38+00:00- author
Alex Chan <alex@alexwlchan.net>- parent
75a3b5f- message
fiddle with album overlays, allow reviewing again- changed files
6 files, 146 additions, 24 deletionsBlinkReviewer/BlinkReviewer.xcodeproj/project.pbxprojBlinkReviewer/BlinkReviewer/Photos/AssetHelpers.swiftBlinkReviewer/BlinkReviewer/Photos/PhotosLibrary.swiftBlinkReviewer/BlinkReviewer/Views/FocusedImage/AlbumInfoOverlay.swiftBlinkReviewer/BlinkReviewer/Views/PHAssetImage.swiftBlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift
Changed files
BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj (31876) → BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj (32342)
diff --git a/BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj b/BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj
index 81d626c..86ca30c 100644
--- a/BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj
+++ b/BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj
@@ -14,6 +14,7 @@
945F17B62A33D7AA004FC479 /* ReviewStateBorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945F17B52A33D7AA004FC479 /* ReviewStateBorder.swift */; };
945F17B82A33DAC7004FC479 /* ReviewStateSaturation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945F17B72A33DAC7004FC479 /* ReviewStateSaturation.swift */; };
94A0835E2A33E49E00238964 /* FocusedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A0835D2A33E49E00238964 /* FocusedImage.swift */; };
+ 94A083612A33E98000238964 /* AlbumInfoOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94A083602A33E98000238964 /* AlbumInfoOverlay.swift */; };
94C5FFF22A33ADD4004ADDF5 /* PHFetchResultCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C5FFF12A33ADD4004ADDF5 /* PHFetchResultCollection.swift */; };
94C5FFF62A33B698004ADDF5 /* PHAssetHStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94C5FFF52A33B698004ADDF5 /* PHAssetHStack.swift */; };
94D2C8B92A320E6F00BEE15B /* ReviewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94D2C8B82A320E6F00BEE15B /* ReviewState.swift */; };
@@ -60,6 +61,7 @@
945F17B52A33D7AA004FC479 /* ReviewStateBorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewStateBorder.swift; sourceTree = "<group>"; };
945F17B72A33DAC7004FC479 /* ReviewStateSaturation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewStateSaturation.swift; sourceTree = "<group>"; };
94A0835D2A33E49E00238964 /* FocusedImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusedImage.swift; sourceTree = "<group>"; };
+ 94A083602A33E98000238964 /* AlbumInfoOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumInfoOverlay.swift; sourceTree = "<group>"; };
94C5FFF12A33ADD4004ADDF5 /* PHFetchResultCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHFetchResultCollection.swift; sourceTree = "<group>"; };
94C5FFF52A33B698004ADDF5 /* PHAssetHStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PHAssetHStack.swift; sourceTree = "<group>"; };
94D2C8B82A320E6F00BEE15B /* ReviewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewState.swift; sourceTree = "<group>"; };
@@ -136,6 +138,7 @@
isa = PBXGroup;
children = (
94A0835D2A33E49E00238964 /* FocusedImage.swift */,
+ 94A083602A33E98000238964 /* AlbumInfoOverlay.swift */,
);
path = FocusedImage;
sourceTree = "<group>";
@@ -374,6 +377,7 @@
945F17B42A33D726004FC479 /* ReviewStateIcon.swift in Sources */,
945F17B62A33D7AA004FC479 /* ReviewStateBorder.swift in Sources */,
94D7511E2A31B243005859E7 /* FullSizeImage.swift in Sources */,
+ 94A083612A33E98000238964 /* AlbumInfoOverlay.swift in Sources */,
94C5FFF22A33ADD4004ADDF5 /* PHFetchResultCollection.swift in Sources */,
94D750F02A31A796005859E7 /* BlinkReviewerApp.swift in Sources */,
940331732A336B5100200C5D /* DeferredRendering.swift in Sources */,
BlinkReviewer/BlinkReviewer/Photos/AssetHelpers.swift (2805) → BlinkReviewer/BlinkReviewer/Photos/AssetHelpers.swift (2360)
diff --git a/BlinkReviewer/BlinkReviewer/Photos/AssetHelpers.swift b/BlinkReviewer/BlinkReviewer/Photos/AssetHelpers.swift
index 3e29e8d..dec461e 100644
--- a/BlinkReviewer/BlinkReviewer/Photos/AssetHelpers.swift
+++ b/BlinkReviewer/BlinkReviewer/Photos/AssetHelpers.swift
@@ -8,21 +8,6 @@
import Foundation
import Photos
-/// Returns a list of all the images in the Photos Library.
-func getAllPhotos() -> [PHAsset] {
- var photos: [PHAsset] = []
-
- let options = PHFetchOptions()
- options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
-
- PHAsset.fetchAssets(with: PHAssetMediaType.image, options: options)
- .enumerateObjects({ (asset, _, _) in
- photos.append(asset)
- })
-
- return photos
-}
-
extension PHAsset {
/// Returns a list of all the albums that contain this asset.
func albums() -> [PHAssetCollection] {
BlinkReviewer/BlinkReviewer/Photos/PhotosLibrary.swift (3802) → BlinkReviewer/BlinkReviewer/Photos/PhotosLibrary.swift (5792)
diff --git a/BlinkReviewer/BlinkReviewer/Photos/PhotosLibrary.swift b/BlinkReviewer/BlinkReviewer/Photos/PhotosLibrary.swift
index 7034eec..c431463 100644
--- a/BlinkReviewer/BlinkReviewer/Photos/PhotosLibrary.swift
+++ b/BlinkReviewer/BlinkReviewer/Photos/PhotosLibrary.swift
@@ -41,21 +41,51 @@ class PhotosLibrary: NSObject, ObservableObject, PHPhotoLibraryChangeObserver {
private func updateStatus(_ changeInstance: PHChange) {
DispatchQueue.main.async {
+ let start = DispatchTime.now()
+ var elapsed = start
+
+ func printElapsed(_ label: String) -> Void {
+ let now = DispatchTime.now()
+
+ let totalInterval = Double(now.uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000_000
+ let elapsedInterval = Double(now.uptimeNanoseconds - elapsed.uptimeNanoseconds) / 1_000_000_000
+
+ elapsed = DispatchTime.now()
+
+ print("Time to \(label):\n \(elapsedInterval) seconds (\(totalInterval) total)")
+ }
+
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
options.fetchLimit = 500
// print()
- if let changeDetails = changeInstance.changeDetails(for: self.assets2) {
- self.assets2 = changeDetails.fetchResultAfterChanges
+ if let assetsChangeDetails = changeInstance.changeDetails(for: self.assets2) {
+ self.assets2 = assetsChangeDetails.fetchResultAfterChanges
} else {
self.assets2 = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: options)
}
-
- self.approvedAssets = PHAsset.fetchAssets(in: self.approved, options: nil)
- self.rejectedAssets = PHAsset.fetchAssets(in: self.rejected, options: nil)
- self.needsActionAssets = PHAsset.fetchAssets(in: self.needsAction, options: nil)
+
+ if let approvedChangeDetails = changeInstance.changeDetails(for: self.approvedAssets) {
+ self.approvedAssets = approvedChangeDetails.fetchResultAfterChanges
+ } else {
+ self.approvedAssets = PHAsset.fetchAssets(in: self.approved, options: nil)
+ }
+
+ if let rejectedChangeDetails = changeInstance.changeDetails(for: self.rejectedAssets) {
+ self.rejectedAssets = rejectedChangeDetails.fetchResultAfterChanges
+ } else {
+ self.rejectedAssets = PHAsset.fetchAssets(in: self.rejected, options: nil)
+ }
+
+ if let needsActionChangeDetails = changeInstance.changeDetails(for: self.needsActionAssets) {
+ self.needsActionAssets = needsActionChangeDetails.fetchResultAfterChanges
+ } else {
+ self.needsActionAssets = PHAsset.fetchAssets(in: self.needsAction, options: nil)
+ }
+
+ printElapsed("get all photos data (update)")
self.isPhotoLibraryAuthorized = PHPhotoLibrary.authorizationStatus() == .authorized
}
@@ -64,6 +94,20 @@ class PhotosLibrary: NSObject, ObservableObject, PHPhotoLibraryChangeObserver {
private func updateStatus(isChange: Bool) {
DispatchQueue.main.async {
+ let start = DispatchTime.now()
+ var elapsed = start
+
+ func printElapsed(_ label: String) -> Void {
+ let now = DispatchTime.now()
+
+ let totalInterval = Double(now.uptimeNanoseconds - start.uptimeNanoseconds) / 1_000_000_000
+ let elapsedInterval = Double(now.uptimeNanoseconds - elapsed.uptimeNanoseconds) / 1_000_000_000
+
+ elapsed = DispatchTime.now()
+
+ print("Time to \(label):\n \(elapsedInterval) seconds (\(totalInterval) total)")
+ }
+
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
options.fetchLimit = 500
@@ -74,6 +118,8 @@ class PhotosLibrary: NSObject, ObservableObject, PHPhotoLibraryChangeObserver {
self.rejectedAssets = PHAsset.fetchAssets(in: self.rejected, options: nil)
self.needsActionAssets = PHAsset.fetchAssets(in: self.needsAction, options: nil)
+ printElapsed("get all photos data (new)")
+
self.isPhotoLibraryAuthorized = PHPhotoLibrary.authorizationStatus() == .authorized
}
}
BlinkReviewer/BlinkReviewer/Views/FocusedImage/AlbumInfoOverlay.swift (0) → BlinkReviewer/BlinkReviewer/Views/FocusedImage/AlbumInfoOverlay.swift (1833)
diff --git a/BlinkReviewer/BlinkReviewer/Views/FocusedImage/AlbumInfoOverlay.swift b/BlinkReviewer/BlinkReviewer/Views/FocusedImage/AlbumInfoOverlay.swift
new file mode 100644
index 0000000..885f4f2
--- /dev/null
+++ b/BlinkReviewer/BlinkReviewer/Views/FocusedImage/AlbumInfoOverlay.swift
@@ -0,0 +1,48 @@
+import SwiftUI
+import Photos
+
+/// Show the names of the albums that a given asset is in.
+///
+/// Each album is shown as a separate "pill" in the list, for example:
+///
+/// [Cats] [Cross-stitch] [Stuff I did in 2023]
+///
+struct AlbumInfoOverlay: ViewModifier {
+ @State var asset: PHAsset?
+
+ // TODO: This doesn't update properly :-/
+ func body(content: Content) -> some View {
+ if let thisAsset = asset {
+ content.overlay(alignment: Alignment(horizontal: .center, vertical: .top)) {
+ HStack {
+ ForEach(thisAsset.albums(), id: \.localIdentifier) { album in
+ if let title = album.localizedTitle {
+ // Don't show the names of the meta-albums used to manage
+ // review state.
+ if (title != "Approved" && title != "Rejected" && title != "Needs Action") {
+
+ // The icon was chosen to match the one used for albums
+ // in the sidebar in Photos.
+ Text("\(Image(systemName: "rectangle.stack")) \(title)")
+ .fontWeight(.bold)
+ .font(.title2)
+ .padding(5)
+ .background(.white.opacity(0.9))
+ .cornerRadius(7.0)
+ .shadow(radius: 2.0)
+ }
+ }
+ }
+ }.padding()
+ }
+ } else {
+ content
+ }
+ }
+}
+
+extension View {
+ func albumInfo(for asset: PHAsset?) -> some View {
+ modifier(AlbumInfoOverlay(asset: asset))
+ }
+}
BlinkReviewer/BlinkReviewer/Views/PHAssetImage.swift (4581) → BlinkReviewer/BlinkReviewer/Views/PHAssetImage.swift (4583)
diff --git a/BlinkReviewer/BlinkReviewer/Views/PHAssetImage.swift b/BlinkReviewer/BlinkReviewer/Views/PHAssetImage.swift
index 8163d28..d0cc05a 100644
--- a/BlinkReviewer/BlinkReviewer/Views/PHAssetImage.swift
+++ b/BlinkReviewer/BlinkReviewer/Views/PHAssetImage.swift
@@ -103,7 +103,7 @@ class PHAssetImage: NSObject, ObservableObject {
}
if let imageResult = result {
- print("got image!")
+// print("got image!")
self.image = imageResult
if !self.isDegraded {
BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift (9584) → BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift (11564)
diff --git a/BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift b/BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift
index 8a0d609..832a67a 100644
--- a/BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift
+++ b/BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift
@@ -45,13 +45,12 @@ struct PhotoReviewer: View {
.frame(height: 90)
FocusedImage(assetImage: focusedAssetImage)
+ .albumInfo(for: focusedAssetImage.asset)
Spacer()
}
}
.onAppear {
-
-
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
handleKeyEventNew(event)
return event
@@ -137,6 +136,46 @@ struct PhotoReviewer: View {
focusedAssetIndex -= 1
}
+ case let e where e.characters == "1" || e.characters == "2" || e.characters == "3":
+ print("time to review!")
+ let state = photosLibrary.state(for: focusedAsset)
+
+ let approved = getAlbum(withName: "Approved")
+ let rejected = getAlbum(withName: "Rejected")
+ let needsAction = getAlbum(withName: "Needs Action")
+
+ 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 state == .Approved {
+ focusedAsset.remove(fromAlbum: approved)
+ } else if e.characters == "1" {
+ focusedAsset.add(toAlbum: approved)
+ }
+
+ if state == .Rejected {
+ focusedAsset.remove(fromAlbum: rejected)
+ } else if e.characters == "2" {
+ focusedAsset.add(toAlbum: rejected)
+ }
+
+ if state == .NeedsAction {
+ focusedAsset.remove(fromAlbum: needsAction)
+ } else if e.characters == "3" {
+ focusedAsset.add(toAlbum: needsAction)
+ }
+ }
+
+ if selectedAssetIndex < photosLibrary.assets2.count - 1 {
+ focusedAssetIndex += 1
+ }
+
default:
logger.info("Received unhandled keyboard event: \(event, privacy: .public)")
break