Skip to content

Commit

Permalink
Merge pull request #1 from dxclancy/main
Browse files Browse the repository at this point in the history
Allow the user to create an album in Photos from the currently filtered/sorted view of the FaceGrid @dxclancy
Better error handling
Toolbar refactor
  • Loading branch information
TimFelixBeyer authored Feb 19, 2024
2 parents 686cb70 + 0d42c0f commit c3f4a55
Show file tree
Hide file tree
Showing 12 changed files with 237 additions and 123 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ DerivedData/
*.ipa
*.dSYM.zip
*.dSYM
.DS_Store

## Playgrounds
timeline.xctimeline
Expand Down
26 changes: 23 additions & 3 deletions FaceExplorer.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@
223B6CA8299BC7F3006F6CC2 /* FaceGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223B6CA6299BC7A6006F6CC2 /* FaceGrid.swift */; };
223B6CAD299BC9A7006F6CC2 /* FaceCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223B6CA9299BC97D006F6CC2 /* FaceCard.swift */; };
223B6CB6299C1B0E006F6CC2 /* Person.swift in Sources */ = {isa = PBXBuildFile; fileRef = 223B6CB5299C1B0D006F6CC2 /* Person.swift */; };
225397D42B7CFE4C00C4D43C /* Toolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225397D32B7CFE4C00C4D43C /* Toolbar.swift */; };
227E35FF29AA546F004BD737 /* FaceCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227E35FE29AA546F004BD737 /* FaceCommands.swift */; };
227E360129AA6228004BD737 /* FaceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 227E360029AA6228004BD737 /* FaceSettings.swift */; };
229450FA29AAD1510041CB2B /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = 229450F929AAD1510041CB2B /* README.md */; };
22A6BF3B29AFBEDE00A443C9 /* Resources in Resources */ = {isa = PBXBuildFile; fileRef = 22A6BF3A29AFBEDE00A443C9 /* Resources */; };
22A6BF3D29B00AD000A443C9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22A6BF3C29B00AD000A443C9 /* AppDelegate.swift */; };
22B4E0D729AE3CC400DA2268 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 22B4E0D629AE3CC400DA2268 /* Assets.xcassets */; };
36527E8B2B19859D002EA155 /* PhotosLibraryAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36527E8A2B19859C002EA155 /* PhotosLibraryAPI.swift */; };
C2D89E6E24B9358D0096C79B /* ModelData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20D780424B684EC002E7AEA /* ModelData.swift */; };
C2D89E7324B935F50096C79B /* FaceExplorerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C20D77ED24B665D7002E7AEA /* FaceExplorerApp.swift */; };
/* End PBXBuildFile section */
Expand All @@ -30,12 +32,14 @@
223B6CA9299BC97D006F6CC2 /* FaceCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaceCard.swift; sourceTree = "<group>"; };
223B6CAB299BC98C006F6CC2 /* FaceDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaceDetail.swift; sourceTree = "<group>"; };
223B6CB5299C1B0D006F6CC2 /* Person.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Person.swift; sourceTree = "<group>"; };
225397D32B7CFE4C00C4D43C /* Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Toolbar.swift; sourceTree = "<group>"; };
227E35FE29AA546F004BD737 /* FaceCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaceCommands.swift; sourceTree = "<group>"; };
227E360029AA6228004BD737 /* FaceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaceSettings.swift; sourceTree = "<group>"; };
229450F929AAD1510041CB2B /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
22A6BF3A29AFBEDE00A443C9 /* Resources */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Resources; sourceTree = "<group>"; };
22A6BF3C29B00AD000A443C9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
22B4E0D629AE3CC400DA2268 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = FaceExplorer/Assets.xcassets; sourceTree = SOURCE_ROOT; };
36527E8A2B19859C002EA155 /* PhotosLibraryAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotosLibraryAPI.swift; sourceTree = "<group>"; };
C20D77ED24B665D7002E7AEA /* FaceExplorerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaceExplorerApp.swift; sourceTree = "<group>"; };
C20D77F624B665D8002E7AEA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
C20D780424B684EC002E7AEA /* ModelData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModelData.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -73,6 +77,14 @@
path = Faces;
sourceTree = "<group>";
};
225397D52B7D0B2300C4D43C /* Toolbar */ = {
isa = PBXGroup;
children = (
225397D32B7CFE4C00C4D43C /* Toolbar.swift */,
);
path = Toolbar;
sourceTree = "<group>";
};
C20D77E124B665D7002E7AEA = {
isa = PBXGroup;
children = (
Expand All @@ -93,10 +105,11 @@
C20D77EC24B665D7002E7AEA /* FaceExplorer */ = {
isa = PBXGroup;
children = (
22A6BF3C29B00AD000A443C9 /* AppDelegate.swift */,
227E35FE29AA546F004BD737 /* FaceCommands.swift */,
227E360029AA6228004BD737 /* FaceSettings.swift */,
C20D77ED24B665D7002E7AEA /* FaceExplorerApp.swift */,
22A6BF3C29B00AD000A443C9 /* AppDelegate.swift */,
36527E8A2B19859C002EA155 /* PhotosLibraryAPI.swift */,
C20D780824B685C4002E7AEA /* Model */,
C2FEB32D24B7C802004FAB73 /* Views */,
C2D89E6A24B9353B0096C79B /* FaceExplorer.entitlements */,
Expand All @@ -122,6 +135,7 @@
children = (
223B6CA5299BC791006F6CC2 /* Faces */,
220A1A4129AECB22009A84EE /* Helpers */,
225397D52B7D0B2300C4D43C /* Toolbar */,
);
path = Views;
sourceTree = "<group>";
Expand Down Expand Up @@ -227,6 +241,8 @@
buildActionMask = 2147483647;
files = (
C2D89E6E24B9358D0096C79B /* ModelData.swift in Sources */,
36527E8B2B19859D002EA155 /* PhotosLibraryAPI.swift in Sources */,
225397D42B7CFE4C00C4D43C /* Toolbar.swift in Sources */,
223B6CAD299BC9A7006F6CC2 /* FaceCard.swift in Sources */,
223B6CA8299BC7F3006F6CC2 /* FaceGrid.swift in Sources */,
C2D89E7324B935F50096C79B /* FaceExplorerApp.swift in Sources */,
Expand Down Expand Up @@ -378,14 +394,16 @@
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = FaceExplorer/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = FaceExplorer;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app needs to access your Photos Library to create an album with your faces.";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 0.0.3;
MARKETING_VERSION = 0.0.4;
PRODUCT_BUNDLE_IDENTIFIER = com.FaceExplorer;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand All @@ -412,14 +430,16 @@
ENABLE_PREVIEWS = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = FaceExplorer/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = FaceExplorer;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.photography";
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "This app needs to access your Photos Library to create an album with your faces.";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.0;
MARKETING_VERSION = 0.0.3;
MARKETING_VERSION = 0.0.4;
PRODUCT_BUNDLE_IDENTIFIER = com.FaceExplorer;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
26 changes: 1 addition & 25 deletions FaceExplorer/Info.plist
Original file line number Diff line number Diff line change
@@ -1,29 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<true/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
<dict/>
</plist>
4 changes: 2 additions & 2 deletions FaceExplorer/Model/Face.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ struct Face: Identifiable, Hashable {
private var centerX: Double
private var centerY: Double
private var size: Double
var name: String?
var name: String

var category: Category
enum Category: String, CaseIterable, Codable {
Expand All @@ -35,7 +35,7 @@ struct Face: Identifiable, Hashable {
centerX: Double,
centerY: Double,
size: Double,
name: String?,
name: String,
captureDate: Date,
attributes: [String: (Int, String)]) {
self.id = id
Expand Down
19 changes: 11 additions & 8 deletions FaceExplorer/Model/ModelData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,17 @@ final class ModelData: ObservableObject {
} catch {
persons = []
faces = []
errorOccurred = true
errorMessage = errorToUserMessage(error: error)
displayError(error: error)
}
}
func sortByDate() { faces.sort { $0.captureDate < $1.captureDate } }
func sortByName() { faces.sort { !$0.name!.isEmpty && ($1.name!.isEmpty || ($0.name! < $1.name!)) } }
func sortByName() { faces.sort { !$0.name.isEmpty && ($1.name.isEmpty || ($0.name < $1.name)) } }
func sortBy(displayName: String) { faces.sort { $0.attributes[displayName]!.0 < $1.attributes[displayName]!.0 }
}
func displayError(error: Error) {
errorMessage = errorToUserMessage(error: error)
errorOccurred = true
}
}

func load<T: Decodable>(_ filename: String) -> T {
Expand Down Expand Up @@ -140,7 +143,7 @@ func getFaces(path: String) throws -> [Face] {
var attributeList: [String: (Int, String)] = [:]
for attribute in faceAttributes {
let val = face[Expression<Int>(attribute.queryName)]
attributeList[attribute.displayName] = (val, attribute.mapping[val]!)
attributeList[attribute.displayName] = (val, attribute.mapping[val] ?? "")
}
faces.append(Face(id: face[pk],
uuid: face[uuid],
Expand All @@ -158,7 +161,7 @@ func getFaces(path: String) throws -> [Face] {
return faces.sorted { $0.captureDate < $1.captureDate }
}

func getName(face: Row, personsDict: [Int: Row]) -> String? {
func getName(face: Row, personsDict: [Int: Row]) -> String {
var person: Expression<Int?>
if #available(macOS 14.0, *) {
person = Expression<Int?>("ZPERSONFORFACE")
Expand All @@ -175,7 +178,7 @@ func getName(face: Row, personsDict: [Int: Row]) -> String? {
personID = newPersonID
personRow = newPersonRow
}
return personRow?[fullName]
return personRow?[fullName] ?? ""
} else {
return ""
}
Expand Down Expand Up @@ -224,9 +227,9 @@ struct FaceAttribute: Hashable, Codable {
var mapping: [Int: String]
}

enum FilterCategory: String, CaseIterable, Identifiable {
enum TagFilterCategory: String, CaseIterable, Identifiable {
case all = "All"
case unnnamed = "Unnamed"
case named = "Named"
var id: FilterCategory { self }
var id: TagFilterCategory { self }
}
12 changes: 3 additions & 9 deletions FaceExplorer/Model/Person.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import Foundation

struct Person: Hashable, Codable, Identifiable {
var id: Int
var name: String?
var name: String

var type: Category
enum Category: String, CaseIterable, Codable {
Expand All @@ -11,13 +9,9 @@ struct Person: Hashable, Codable, Identifiable {
case favorite = "Favorite"
}

public init(id: Int, name: String?, type: Int) throws {
public init(id: Int, name: String, type: Int) throws {
self.id = id
if name == "" {
self.name = nil
} else {
self.name = name
}
self.name = name

switch type {
case -1: self.type = .hidden
Expand Down
47 changes: 47 additions & 0 deletions FaceExplorer/PhotosLibraryAPI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// PhotoLibraryAPI.swift
// FaceExplorer
//
// Created on 11/27/23.

import Foundation
import Photos

struct PhotosLibraryAPI {
/// Create a new album in Photos.app with chosen photos
/// using the native Photos API.
/// This brings about some challenges, as the native API always operates on the system library,
/// while FaceExplorer can use any .photoslibrary file.
/// - Parameters:
/// - name: the name of the new album
/// - localIdentifiers: List of localIdentiers for photos to be added to the album
func createAlbum(name: String, withLocalIdentifiers localIdentifiers: [String]) async throws {
let allPhotosOptions = PHFetchOptions()
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
allPhotosOptions.predicate = NSPredicate(format: "localIdentifier in %@", localIdentifiers)
let phAssetsToAdd = PHAsset.fetchAssets(with: allPhotosOptions)

// Make an album with the assets
var newAlbumRequest: PHAssetCollectionChangeRequest?
var albumLocalIdentifier: String?
try await PHPhotoLibrary.shared().performChanges {
newAlbumRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: name)
albumLocalIdentifier = newAlbumRequest!.placeholderForCreatedAssetCollection.localIdentifier
}
if albumLocalIdentifier == nil {
throw AlbumCreationError.runtimeError("Failed to create album: \(name)")
}

try await PHPhotoLibrary.shared().performChanges {
let newlyCreatedAlbum = PHAssetCollection.fetchAssetCollections(
withLocalIdentifiers: [albumLocalIdentifier!], options: nil
).firstObject
let addToAlbumChangeRequest = PHAssetCollectionChangeRequest(for: newlyCreatedAlbum!)
addToAlbumChangeRequest!.addAssets(phAssetsToAdd)
}
}

enum AlbumCreationError: Error {
case runtimeError(String)
}
}
4 changes: 2 additions & 2 deletions FaceExplorer/Resources/FaceAttributeQueries.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "Age",
"queryName": "ZAGETYPE",
"type": "categorical",
"mapping": {"-1": "All", "1": "Baby", "2": "Child", "3": "Young adult", "4": "Adult", "5": "Senior"},
"mapping": {"-1": "All", "1": "Baby", "2": "Child", "3": "Young adult", "5": "Adult", "4": "Senior"},
},
{
"displayName": "Ethnicity",
Expand Down Expand Up @@ -39,7 +39,7 @@
"displayName": "Gaze",
"queryName": "ZGAZETYPE",
"type": "categorical",
"mapping": {"-1": "All", "1": "Into Camera", "2": "Sideways?", "3": "Downwards", "4": "Diagonal?", "5": "Sunglasses"}
"mapping": {"-1": "All", "1": "Into Camera", "2": "Sideways?", "3": "Downwards", "4": "Diagonal?", "5": "Unclear"}
},
{
"displayName": "Gender",
Expand Down
2 changes: 1 addition & 1 deletion FaceExplorer/Views/Faces/FaceCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ struct FaceCard: View {
}
.frame(minWidth: 150, alignment: .leading)
if visibility["Name"]! {
TextField(face.name ?? "Max Mustermann", text: $textFieldInput)
TextField(face.name, text: $textFieldInput)
.focused(focusedField, equals: face.uuid)
.onChange(of: $textFieldInput.wrappedValue, perform: { newValue in
candidates = names.filter({ $0.starts(with: newValue) }).sorted()
Expand Down
Loading

0 comments on commit c3f4a55

Please sign in to comment.