Break up all the thumbnail bits
- ID
5fda324- date
2023-06-09 22:13:07+00:00- author
Alex Chan <alex@alexwlchan.net>- parent
d7a568b- message
Break up all the thumbnail bits- changed files
8 files, 189 additions, 29 deletionsBlinkReviewer/BlinkReviewer.xcodeproj/project.pbxprojBlinkReviewer/BlinkReviewer/Views/Helpers/PHAssetHStack.swiftBlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swiftBlinkReviewer/BlinkReviewer/Views/Thumbnails/FavoriteOverlay.swiftBlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailImage.swiftBlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateBorder.swiftBlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateIcon.swiftBlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateSaturation.swift
Changed files
BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj (29350) → BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj (31238)
diff --git a/BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj b/BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj
index 363ac31..97aaaa5 100644
--- a/BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj
+++ b/BlinkReviewer/BlinkReviewer.xcodeproj/project.pbxproj
@@ -9,6 +9,10 @@
/* Begin PBXBuildFile section */
940331732A336B5100200C5D /* DeferredRendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 940331722A336B5100200C5D /* DeferredRendering.swift */; };
945F17B02A33D167004FC479 /* NewThumbnailImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945F17AF2A33D167004FC479 /* NewThumbnailImage.swift */; };
+ 945F17B22A33D69B004FC479 /* FavoriteOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945F17B12A33D69B004FC479 /* FavoriteOverlay.swift */; };
+ 945F17B42A33D726004FC479 /* ReviewStateIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945F17B32A33D726004FC479 /* ReviewStateIcon.swift */; };
+ 945F17B62A33D7AA004FC479 /* ReviewStateBorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945F17B52A33D7AA004FC479 /* ReviewStateBorder.swift */; };
+ 945F17B82A33DAC7004FC479 /* ReviewStateSaturation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 945F17B72A33DAC7004FC479 /* ReviewStateSaturation.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 */; };
@@ -50,6 +54,10 @@
/* Begin PBXFileReference section */
940331722A336B5100200C5D /* DeferredRendering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeferredRendering.swift; sourceTree = "<group>"; };
945F17AF2A33D167004FC479 /* NewThumbnailImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewThumbnailImage.swift; sourceTree = "<group>"; };
+ 945F17B12A33D69B004FC479 /* FavoriteOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteOverlay.swift; sourceTree = "<group>"; };
+ 945F17B32A33D726004FC479 /* ReviewStateIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewStateIcon.swift; sourceTree = "<group>"; };
+ 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>"; };
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>"; };
@@ -114,6 +122,10 @@
isa = PBXGroup;
children = (
945F17AF2A33D167004FC479 /* NewThumbnailImage.swift */,
+ 945F17B12A33D69B004FC479 /* FavoriteOverlay.swift */,
+ 945F17B32A33D726004FC479 /* ReviewStateIcon.swift */,
+ 945F17B52A33D7AA004FC479 /* ReviewStateBorder.swift */,
+ 945F17B72A33DAC7004FC479 /* ReviewStateSaturation.swift */,
);
path = Thumbnails;
sourceTree = "<group>";
@@ -347,12 +359,16 @@
945F17B02A33D167004FC479 /* NewThumbnailImage.swift in Sources */,
94D2C8BF2A3299BD00BEE15B /* PhotosLibrary.swift in Sources */,
94C5FFF62A33B698004ADDF5 /* PHAssetHStack.swift in Sources */,
+ 945F17B22A33D69B004FC479 /* FavoriteOverlay.swift in Sources */,
+ 945F17B42A33D726004FC479 /* ReviewStateIcon.swift in Sources */,
+ 945F17B62A33D7AA004FC479 /* ReviewStateBorder.swift in Sources */,
94D7511E2A31B243005859E7 /* FullSizeImage.swift in Sources */,
94C5FFF22A33ADD4004ADDF5 /* PHFetchResultCollection.swift in Sources */,
94D750F02A31A796005859E7 /* BlinkReviewerApp.swift in Sources */,
940331732A336B5100200C5D /* DeferredRendering.swift in Sources */,
94D751202A31B53E005859E7 /* AlbumInfo.swift in Sources */,
94D751302A31DC4A005859E7 /* ThumbnailList.swift in Sources */,
+ 945F17B82A33DAC7004FC479 /* ReviewStateSaturation.swift in Sources */,
94D751222A31BD8E005859E7 /* PhotoReviewer.swift in Sources */,
94D2C8B92A320E6F00BEE15B /* ReviewState.swift in Sources */,
94F7E39E2A331A9E00763DB9 /* Statistics.swift in Sources */,
BlinkReviewer/BlinkReviewer/Views/Helpers/PHAssetHStack.swift (3751) → BlinkReviewer/BlinkReviewer/Views/Helpers/PHAssetHStack.swift (3751)
diff --git a/BlinkReviewer/BlinkReviewer/Views/Helpers/PHAssetHStack.swift b/BlinkReviewer/BlinkReviewer/Views/Helpers/PHAssetHStack.swift
index 0e8e0d8..7595b8d 100644
--- a/BlinkReviewer/BlinkReviewer/Views/Helpers/PHAssetHStack.swift
+++ b/BlinkReviewer/BlinkReviewer/Views/Helpers/PHAssetHStack.swift
@@ -35,7 +35,7 @@ struct PHAssetHStack<Content: View>: View {
var body: some View {
ScrollView(.horizontal) {
- LazyHStack(spacing: 5) {
+ LazyHStack(spacing: 7) {
// Implementation note: we use the localIdentifier rather than the
// array index as the id here, because the app gets way slower if
// you use the PHFetchResult index -- it tries to regenerate a bunch of
BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift (8684) → BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift (7686)
diff --git a/BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift b/BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift
index 26f7acb..d3cb34a 100644
--- a/BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift
+++ b/BlinkReviewer/BlinkReviewer/Views/PhotoReviewer.swift
@@ -34,25 +34,8 @@ struct PhotoReviewer: View {
ZStack {
VStack {
PHAssetHStack(photosLibrary.assets2) { asset, index in
- VStack {
-
- NewThumbnailImage(asset)
-// .resizable()
- .saturation(photosLibrary.state(for: asset) == .Rejected ? 0.0 : 1.0)
- // Note: it's taken several attempts to get this working correctly;
- // it behaves differently in the running app to the SwiftUI preview.
- //
- // Expected properties:
- //
- // - Thumbnails are square
- // - Thumbnails are expanded to fill the square, but they prefer
- // to crop rather than stretch the image
- //
- .scaledToFill()
- .frame(width: 70.0, height: 70.0, alignment: .center)
- .border(.green)
-// Text("\(index) / \(asset.localIdentifier)")
- }
+ NewThumbnailImage(asset, isFocused: index == focusedAssetIndex)
+ .environmentObject(photosLibrary)
}
}
//
BlinkReviewer/BlinkReviewer/Views/Thumbnails/FavoriteOverlay.swift (0) → BlinkReviewer/BlinkReviewer/Views/Thumbnails/FavoriteOverlay.swift (804)
diff --git a/BlinkReviewer/BlinkReviewer/Views/Thumbnails/FavoriteOverlay.swift b/BlinkReviewer/BlinkReviewer/Views/Thumbnails/FavoriteOverlay.swift
new file mode 100644
index 0000000..ee93bfa
--- /dev/null
+++ b/BlinkReviewer/BlinkReviewer/Views/Thumbnails/FavoriteOverlay.swift
@@ -0,0 +1,30 @@
+import SwiftUI
+import Photos
+
+/// Renders a small heart to indicate a photo is a "Favorite".
+///
+/// This is meant to match the way favorite items are marked in Photos.
+struct FavoriteHeartIcon: ViewModifier {
+ let asset: PHAsset
+
+ init(_ asset: PHAsset) {
+ self.asset = asset
+ }
+
+ func body(content: Content) -> some View {
+ content.overlay(alignment: Alignment(horizontal: .leading, vertical: .bottom)) {
+ if asset.isFavorite {
+ Image(systemName: "heart.fill")
+ .foregroundColor(.white)
+ .padding(2)
+ .shadow(radius: 2.0)
+ }
+ }
+ }
+}
+
+extension View {
+ func favoriteHeartIcon(for asset: PHAsset) -> some View {
+ modifier(FavoriteHeartIcon(asset))
+ }
+}
BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailImage.swift (492) → BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailImage.swift (2012)
diff --git a/BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailImage.swift b/BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailImage.swift
index a2b74a2..d8d96f7 100644
--- a/BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailImage.swift
+++ b/BlinkReviewer/BlinkReviewer/Views/Thumbnails/NewThumbnailImage.swift
@@ -1,23 +1,64 @@
-//
-// NewThumbnailImage.swift
-// BlinkReviewer
-//
-// Created by Alex Chan on 09/06/2023.
-//
-
import SwiftUI
import Photos
+/// Render a single thumbnail image in the thumbnail picker.
+///
+/// Thumbnails are square, and they expand to fill the square. This may
+/// mean some information gets cropped out -- that's okay, these are only
+/// small previews, not complete images.
struct NewThumbnailImage: View {
+ @EnvironmentObject var photosLibrary: PhotosLibrary
+
var asset: PHAsset
+ var isFocused: Bool
+
+ private var size: CGFloat
+ private var cornerRadius: CGFloat
+
@ObservedObject var assetImage: PHAssetImage
- init(_ asset: PHAsset) {
+ init(_ asset: PHAsset, isFocused: Bool) {
self.asset = asset
- self.assetImage = PHAssetImage(asset, size: CGSize(width: 70.0, height: 70.0), deliveryMode: .fastFormat)
+ self.isFocused = isFocused
+
+ self.size = isFocused ? 70 : 50
+ self.cornerRadius = isFocused ? 7 : 5
+
+ self.assetImage = PHAssetImage(
+ asset,
+ size: CGSize(width: self.size, height: self.size),
+ deliveryMode: .fastFormat
+ )
+ }
+
+ private var state: ReviewState? {
+ photosLibrary.state(for: asset)
}
var body: some View {
Image(nsImage: assetImage.image)
+ .resizable()
+ .scaledToFill()
+ .clipped()
+ .frame(width: self.size, height: self.size, alignment: .center)
+ .cornerRadius(cornerRadius)
+ .reviewStateBorder(for: state, with: cornerRadius)
+ .reviewStateIcon(for: state)
+ .reviewStateColor(isRejected: state == .Rejected)
+ .favoriteHeartIcon(for: asset)
+ }
+}
+
+struct NewThumbnailImage_Previews: PreviewProvider {
+ static var asset: PHAsset = PHAsset.fetchAssets(with: nil).firstObject!
+
+ static var previews: some View {
+ NewThumbnailImage(asset, isFocused: false)
+ .environmentObject(PhotosLibrary())
+ .previewDisplayName("thumbnail, not focused")
+
+ NewThumbnailImage(asset, isFocused: true)
+ .environmentObject(PhotosLibrary())
+ .previewDisplayName("thumbnail, focused")
}
}
BlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateBorder.swift (0) → BlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateBorder.swift (1049)
diff --git a/BlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateBorder.swift b/BlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateBorder.swift
new file mode 100644
index 0000000..a65d4fc
--- /dev/null
+++ b/BlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateBorder.swift
@@ -0,0 +1,34 @@
+import SwiftUI
+
+import SwiftUI
+
+/// Renders a small icon to show the review state, e.g. a green circled tick
+/// for "Approved" images.
+struct ReviewStateBorder: ViewModifier {
+ let state: ReviewState?
+ let cornerRadius: CGFloat
+
+ init(_ state: ReviewState?, _ cornerRadius: CGFloat) {
+ self.state = state
+ self.cornerRadius = cornerRadius
+ }
+
+ func body(content: Content) -> some View {
+ content.overlay() {
+ // This technique for drawing a coloured border with rounded corners
+ // comes from an article by Simon Ng:
+ // https://www.appcoda.com/swiftui-border/
+ RoundedRectangle(cornerRadius: cornerRadius)
+ .stroke(
+ state?.color() ?? .gray.opacity(0.7),
+ lineWidth: state != nil ? 3.0 : 1.0
+ )
+ }
+ }
+}
+
+extension View {
+ func reviewStateBorder(for state: ReviewState?, with cornerRadius: CGFloat) -> some View {
+ modifier(ReviewStateBorder(state, cornerRadius))
+ }
+}
BlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateIcon.swift (0) → BlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateIcon.swift (893)
diff --git a/BlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateIcon.swift b/BlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateIcon.swift
new file mode 100644
index 0000000..1133501
--- /dev/null
+++ b/BlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateIcon.swift
@@ -0,0 +1,32 @@
+import SwiftUI
+
+/// Renders a small icon to show the review state, e.g. a green circled tick
+/// for "Approved" images.
+struct ReviewStateIcon: ViewModifier {
+ let state: ReviewState?
+
+ init(_ state: ReviewState?) {
+ self.state = state
+ }
+
+ func body(content: Content) -> some View {
+ if let thisState = state {
+ content.overlay(alignment: Alignment(horizontal: .leading, vertical: .top)) {
+ thisState.icon()
+ .foregroundStyle(.white, thisState.color())
+ .symbolRenderingMode(.palette)
+ .padding(2)
+ .font(.title2)
+ .shadow(radius: 2.0)
+ }
+ } else {
+ content
+ }
+ }
+}
+
+extension View {
+ func reviewStateIcon(for state: ReviewState?) -> some View {
+ modifier(ReviewStateIcon(state))
+ }
+}
BlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateSaturation.swift (0) → BlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateSaturation.swift (502)
diff --git a/BlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateSaturation.swift b/BlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateSaturation.swift
new file mode 100644
index 0000000..1dbfb14
--- /dev/null
+++ b/BlinkReviewer/BlinkReviewer/Views/Thumbnails/ReviewStateSaturation.swift
@@ -0,0 +1,24 @@
+import SwiftUI
+
+/// Desaturates rejected images.
+struct ReviewStateSaturation: ViewModifier {
+ let isRejected: Bool
+
+ init(_ isRejected: Bool) {
+ self.isRejected = isRejected
+ }
+
+ func body(content: Content) -> some View {
+ if isRejected {
+ content.saturation(0.0)
+ } else {
+ content
+ }
+ }
+}
+
+extension View {
+ func reviewStateColor(isRejected: Bool) -> some View {
+ modifier(ReviewStateSaturation(isRejected))
+ }
+}