Version 1
This commit is contained in:
parent
bd63eb38e2
commit
2806733b71
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
/build/*
|
||||
/Pods/*
|
||||
Podfile.lock
|
@ -8,6 +8,14 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
043EC7C35065DD26F6BB496F /* Pods_CapCollector.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 86546C4DAB5E47A540F6E8DD /* Pods_CapCollector.framework */; };
|
||||
5904C33A2199C9FA0046A573 /* SortController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5904C3392199C9FA0046A573 /* SortController.swift */; };
|
||||
5904C33C2199D0260046A573 /* AlwaysShowPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */; };
|
||||
591252EE21A837FB005B1179 /* Squeezenet.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = 591252EB21A837FB005B1179 /* Squeezenet.mlmodel */; };
|
||||
591252F021A837FB005B1179 /* Resnet.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = 591252ED21A837FB005B1179 /* Resnet.mlmodel */; };
|
||||
59158B1621E37B0200D90CB0 /* GridViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59158B1521E37B0200D90CB0 /* GridViewController.swift */; };
|
||||
59158B1821E4C9AC00D90CB0 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59158B1721E4C9AC00D90CB0 /* NavigationController.swift */; };
|
||||
591832CE21A2A97E00E5987D /* Cap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591832CD21A2A97E00E5987D /* Cap.swift */; };
|
||||
598D60E221B6B4D200C7473E /* ImageClassifier.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = 598D60E121B6B4D200C7473E /* ImageClassifier.mlmodel */; };
|
||||
59C1BBA92174CBB800EC84BB /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C1BBA82174CBB800EC84BB /* SettingsController.swift */; };
|
||||
59C1BBAB21762D9600EC84BB /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59C1BBAA21762D9600EC84BB /* UserDefaults.swift */; };
|
||||
CE56CECE209D81DE00932C01 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CECD209D81DE00932C01 /* AppDelegate.swift */; };
|
||||
@ -15,9 +23,6 @@
|
||||
CE56CED5209D81E000932C01 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE56CED4209D81E000932C01 /* Assets.xcassets */; };
|
||||
CE56CED8209D81E000932C01 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE56CED6209D81E000932C01 /* LaunchScreen.storyboard */; };
|
||||
CE56CEF8209D83B800932C01 /* CapCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE0209D83B200932C01 /* CapCell.swift */; };
|
||||
CE56CEF9209D83B800932C01 /* CapImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE1209D83B200932C01 /* CapImages.swift */; };
|
||||
CE56CEFA209D83B800932C01 /* CapNames.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE2209D83B200932C01 /* CapNames.swift */; };
|
||||
CE56CEFC209D83B800932C01 /* DropboxUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE4209D83B300932C01 /* DropboxUpload.swift */; };
|
||||
CE56CEFD209D83B800932C01 /* NameFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE5209D83B300932C01 /* NameFile.swift */; };
|
||||
CE56CEFE209D83B800932C01 /* RoundedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE6209D83B300932C01 /* RoundedButton.swift */; };
|
||||
CE56CEFF209D83B800932C01 /* CameraController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE7209D83B300932C01 /* CameraController.swift */; };
|
||||
@ -33,16 +38,21 @@
|
||||
CE56CF09209D83B800932C01 /* Classifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF1209D83B500932C01 /* Classifier.swift */; };
|
||||
CE56CF0A209D83B800932C01 /* CropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF2209D83B600932C01 /* CropView.swift */; };
|
||||
CE56CF0B209D83B800932C01 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF3209D83B600932C01 /* Logger.swift */; };
|
||||
CE56CF0C209D83B800932C01 /* DropboxDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF4209D83B600932C01 /* DropboxDownload.swift */; };
|
||||
CE56CF0D209D83B800932C01 /* ImageSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF5209D83B600932C01 /* ImageSelector.swift */; };
|
||||
CE56CF0E209D83B800932C01 /* PhotoCaptureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF6209D83B700932C01 /* PhotoCaptureHandler.swift */; };
|
||||
CE56CF0F209D83B800932C01 /* UIImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF7209D83B700932C01 /* UIImageExtensions.swift */; };
|
||||
CEF38744209D8476001C8D3C /* Resnet.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = CEF38742209D8476001C8D3C /* Resnet.mlmodel */; };
|
||||
CEF38745209D8476001C8D3C /* Squeezenet.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = CEF38743209D8476001C8D3C /* Squeezenet.mlmodel */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
342A23CD7996DA1E7039C097 /* Pods-CapCollector.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CapCollector.release.xcconfig"; path = "Pods/Target Support Files/Pods-CapCollector/Pods-CapCollector.release.xcconfig"; sourceTree = "<group>"; };
|
||||
5904C3392199C9FA0046A573 /* SortController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortController.swift; sourceTree = "<group>"; };
|
||||
5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlwaysShowPopup.swift; sourceTree = "<group>"; };
|
||||
591252EB21A837FB005B1179 /* Squeezenet.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = Squeezenet.mlmodel; path = ../../../../Dropbox/Models/Squeezenet.mlmodel; sourceTree = "<group>"; };
|
||||
591252ED21A837FB005B1179 /* Resnet.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = Resnet.mlmodel; path = ../../../../Dropbox/Models/Resnet.mlmodel; sourceTree = "<group>"; };
|
||||
59158B1521E37B0200D90CB0 /* GridViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridViewController.swift; sourceTree = "<group>"; };
|
||||
59158B1721E4C9AC00D90CB0 /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = "<group>"; };
|
||||
591832CD21A2A97E00E5987D /* Cap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cap.swift; sourceTree = "<group>"; };
|
||||
598D60E121B6B4D200C7473E /* ImageClassifier.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = ImageClassifier.mlmodel; path = ../../../../Dropbox/Models/ImageClassifier.mlmodel; sourceTree = "<group>"; };
|
||||
59C1BBA82174CBB800EC84BB /* SettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = "<group>"; };
|
||||
59C1BBAA21762D9600EC84BB /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = "<group>"; };
|
||||
86546C4DAB5E47A540F6E8DD /* Pods_CapCollector.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CapCollector.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -53,9 +63,6 @@
|
||||
CE56CED7209D81E000932C01 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
CE56CED9209D81E000932C01 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
CE56CEE0209D83B200932C01 /* CapCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CapCell.swift; sourceTree = "<group>"; };
|
||||
CE56CEE1209D83B200932C01 /* CapImages.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CapImages.swift; sourceTree = "<group>"; };
|
||||
CE56CEE2209D83B200932C01 /* CapNames.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CapNames.swift; sourceTree = "<group>"; };
|
||||
CE56CEE4209D83B300932C01 /* DropboxUpload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropboxUpload.swift; sourceTree = "<group>"; };
|
||||
CE56CEE5209D83B300932C01 /* NameFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NameFile.swift; sourceTree = "<group>"; };
|
||||
CE56CEE6209D83B300932C01 /* RoundedButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedButton.swift; sourceTree = "<group>"; };
|
||||
CE56CEE7209D83B300932C01 /* CameraController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraController.swift; sourceTree = "<group>"; };
|
||||
@ -71,13 +78,9 @@
|
||||
CE56CEF1209D83B500932C01 /* Classifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Classifier.swift; sourceTree = "<group>"; };
|
||||
CE56CEF2209D83B600932C01 /* CropView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropView.swift; sourceTree = "<group>"; };
|
||||
CE56CEF3209D83B600932C01 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
CE56CEF4209D83B600932C01 /* DropboxDownload.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropboxDownload.swift; sourceTree = "<group>"; };
|
||||
CE56CEF5209D83B600932C01 /* ImageSelector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageSelector.swift; sourceTree = "<group>"; };
|
||||
CE56CEF6209D83B700932C01 /* PhotoCaptureHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureHandler.swift; sourceTree = "<group>"; };
|
||||
CE56CEF7209D83B700932C01 /* UIImageExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtensions.swift; sourceTree = "<group>"; };
|
||||
CE92CA6120CAD38600D5DAA4 /* Xcode.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; path = Xcode.mlmodel; sourceTree = "<group>"; };
|
||||
CEF38742209D8476001C8D3C /* Resnet.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = Resnet.mlmodel; path = ../../../../Dropbox/Models/Resnet.mlmodel; sourceTree = "<group>"; };
|
||||
CEF38743209D8476001C8D3C /* Squeezenet.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = Squeezenet.mlmodel; path = ../../../../Dropbox/Models/Squeezenet.mlmodel; sourceTree = "<group>"; };
|
||||
DBD72193E502C23E06DA913D /* Pods-CapCollector.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CapCollector.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CapCollector/Pods-CapCollector.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
@ -102,6 +105,16 @@
|
||||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
591252E921A837B4005B1179 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
591252ED21A837FB005B1179 /* Resnet.mlmodel */,
|
||||
591252EB21A837FB005B1179 /* Squeezenet.mlmodel */,
|
||||
598D60E121B6B4D200C7473E /* ImageClassifier.mlmodel */,
|
||||
);
|
||||
name = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9EAE4B3CEE704AF443897B44 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -132,10 +145,9 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE56CECD209D81DE00932C01 /* AppDelegate.swift */,
|
||||
CEF38742209D8476001C8D3C /* Resnet.mlmodel */,
|
||||
CEF38743209D8476001C8D3C /* Squeezenet.mlmodel */,
|
||||
CE92CA6120CAD38600D5DAA4 /* Xcode.mlmodel */,
|
||||
CE56CED1209D81DE00932C01 /* Main.storyboard */,
|
||||
CE56CEF1209D83B500932C01 /* Classifier.swift */,
|
||||
591252E921A837B4005B1179 /* Models */,
|
||||
CEF3874D209D9378001C8D3C /* Capture */,
|
||||
CEF3874E209D9390001C8D3C /* Sync */,
|
||||
CEF38750209D93D1001C8D3C /* Data */,
|
||||
@ -143,8 +155,6 @@
|
||||
CEF3874F209D93A6001C8D3C /* Presentation */,
|
||||
CEF3874C209D935E001C8D3C /* Extensions */,
|
||||
CE56CEF3209D83B600932C01 /* Logger.swift */,
|
||||
CE56CED1209D81DE00932C01 /* Main.storyboard */,
|
||||
59C1BBA82174CBB800EC84BB /* SettingsController.swift */,
|
||||
CE56CEDF209D81FD00932C01 /* Support */,
|
||||
);
|
||||
path = CapCollector;
|
||||
@ -163,6 +173,7 @@
|
||||
CEF3874B209D932E001C8D3C /* View Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */,
|
||||
CE56CEF2209D83B600932C01 /* CropView.swift */,
|
||||
CE56CEEA209D83B400932C01 /* RoundedImageView.swift */,
|
||||
CE56CEE6209D83B300932C01 /* RoundedButton.swift */,
|
||||
@ -195,8 +206,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE56CEE9209D83B400932C01 /* DropBoxController.swift */,
|
||||
CE56CEF4209D83B600932C01 /* DropboxDownload.swift */,
|
||||
CE56CEE4209D83B300932C01 /* DropboxUpload.swift */,
|
||||
);
|
||||
path = Sync;
|
||||
sourceTree = "<group>";
|
||||
@ -205,9 +214,13 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE56CEEB209D83B400932C01 /* TableView.swift */,
|
||||
59158B1721E4C9AC00D90CB0 /* NavigationController.swift */,
|
||||
CE56CEE0209D83B200932C01 /* CapCell.swift */,
|
||||
CE56CEF5209D83B600932C01 /* ImageSelector.swift */,
|
||||
CE56CEEF209D83B500932C01 /* ImageCell.swift */,
|
||||
59C1BBA82174CBB800EC84BB /* SettingsController.swift */,
|
||||
5904C3392199C9FA0046A573 /* SortController.swift */,
|
||||
59158B1521E37B0200D90CB0 /* GridViewController.swift */,
|
||||
);
|
||||
path = Presentation;
|
||||
sourceTree = "<group>";
|
||||
@ -215,9 +228,8 @@
|
||||
CEF38750209D93D1001C8D3C /* Data */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE56CEE1209D83B200932C01 /* CapImages.swift */,
|
||||
59C1BBAA21762D9600EC84BB /* UserDefaults.swift */,
|
||||
CE56CEE2209D83B200932C01 /* CapNames.swift */,
|
||||
591832CD21A2A97E00E5987D /* Cap.swift */,
|
||||
CE56CEF0209D83B500932C01 /* DiskManager.swift */,
|
||||
CE56CEE5209D83B300932C01 /* NameFile.swift */,
|
||||
);
|
||||
@ -258,7 +270,12 @@
|
||||
TargetAttributes = {
|
||||
CE56CEC9209D81DD00932C01 = {
|
||||
CreatedOnToolsVersion = 9.4;
|
||||
LastSwiftMigration = 1000;
|
||||
LastSwiftMigration = 1020;
|
||||
SystemCapabilities = {
|
||||
com.apple.BackgroundModes = {
|
||||
enabled = 0;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
@ -340,33 +357,35 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
59C1BBA92174CBB800EC84BB /* SettingsController.swift in Sources */,
|
||||
591252EE21A837FB005B1179 /* Squeezenet.mlmodel in Sources */,
|
||||
CE56CF09209D83B800932C01 /* Classifier.swift in Sources */,
|
||||
5904C33A2199C9FA0046A573 /* SortController.swift in Sources */,
|
||||
CE56CF0B209D83B800932C01 /* Logger.swift in Sources */,
|
||||
CE56CF01209D83B800932C01 /* DropBoxController.swift in Sources */,
|
||||
CE56CF04209D83B800932C01 /* UIViewExtensions.swift in Sources */,
|
||||
59158B1821E4C9AC00D90CB0 /* NavigationController.swift in Sources */,
|
||||
591832CE21A2A97E00E5987D /* Cap.swift in Sources */,
|
||||
CE56CF08209D83B800932C01 /* DiskManager.swift in Sources */,
|
||||
CE56CF0F209D83B800932C01 /* UIImageExtensions.swift in Sources */,
|
||||
CE56CEF9209D83B800932C01 /* CapImages.swift in Sources */,
|
||||
CEF38745209D8476001C8D3C /* Squeezenet.mlmodel in Sources */,
|
||||
CEF38744209D8476001C8D3C /* Resnet.mlmodel in Sources */,
|
||||
CE56CF03209D83B800932C01 /* TableView.swift in Sources */,
|
||||
59158B1621E37B0200D90CB0 /* GridViewController.swift in Sources */,
|
||||
CE56CEFD209D83B800932C01 /* NameFile.swift in Sources */,
|
||||
59C1BBAB21762D9600EC84BB /* UserDefaults.swift in Sources */,
|
||||
CE56CECE209D81DE00932C01 /* AppDelegate.swift in Sources */,
|
||||
CE56CF0D209D83B800932C01 /* ImageSelector.swift in Sources */,
|
||||
598D60E221B6B4D200C7473E /* ImageClassifier.mlmodel in Sources */,
|
||||
CE56CEFF209D83B800932C01 /* CameraController.swift in Sources */,
|
||||
CE56CF05209D83B800932C01 /* ViewControllerExtensions.swift in Sources */,
|
||||
CE56CF0E209D83B800932C01 /* PhotoCaptureHandler.swift in Sources */,
|
||||
CE56CEFE209D83B800932C01 /* RoundedButton.swift in Sources */,
|
||||
CE56CEFA209D83B800932C01 /* CapNames.swift in Sources */,
|
||||
CE56CF07209D83B800932C01 /* ImageCell.swift in Sources */,
|
||||
CE56CF00209D83B800932C01 /* UIAlertControllerExtensions.swift in Sources */,
|
||||
CE56CF06209D83B800932C01 /* CameraView.swift in Sources */,
|
||||
CE56CF0A209D83B800932C01 /* CropView.swift in Sources */,
|
||||
CE56CF0C209D83B800932C01 /* DropboxDownload.swift in Sources */,
|
||||
5904C33C2199D0260046A573 /* AlwaysShowPopup.swift in Sources */,
|
||||
CE56CF02209D83B800932C01 /* RoundedImageView.swift in Sources */,
|
||||
CE56CEFC209D83B800932C01 /* DropboxUpload.swift in Sources */,
|
||||
CE56CEF8209D83B800932C01 /* CapCell.swift in Sources */,
|
||||
591252F021A837FB005B1179 /* Resnet.mlmodel in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -0,0 +1,19 @@
|
||||
<?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>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>CapCollector.xcscheme</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
<key>CapCollector.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
10
CapCollector.xcworkspace/contents.xcworkspacedata
generated
Normal file
10
CapCollector.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:CapCollector.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
@ -0,0 +1,8 @@
|
||||
<?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>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
@ -0,0 +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">
|
||||
<array/>
|
||||
</plist>
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Bucket
|
||||
type = "0"
|
||||
version = "2.0">
|
||||
</Bucket>
|
@ -10,9 +10,29 @@ import UIKit
|
||||
import CoreData
|
||||
import SwiftyDropbox
|
||||
|
||||
var shouldLaunchCamera = false
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
enum ShortcutIdentifier: String {
|
||||
case first
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
init?(fullType: String) {
|
||||
guard let last = fullType.components(separatedBy: ".").last else { return nil }
|
||||
self.init(rawValue: last)
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
var type: String {
|
||||
return Bundle.main.bundleIdentifier! + ".\(self.rawValue)"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Static Properties
|
||||
|
||||
/// Main tint color of the app
|
||||
static let tintColor = UIColor(red: 122/255, green: 155/255, blue: 41/255, alpha: 1)
|
||||
@ -26,6 +46,37 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
return true
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
|
||||
DropboxController.shared.handle(url: url)
|
||||
return true
|
||||
}
|
||||
|
||||
private func handleShortCutItem(_ shortcutItem: UIApplicationShortcutItem) -> Bool {
|
||||
event("Shortcut pressed")
|
||||
shouldLaunchCamera = true
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||
Cap.uploadRemainingImages()
|
||||
guard shouldLaunchCamera else { return }
|
||||
shouldLaunchCamera = false
|
||||
if let c = (frontmostViewController as? UINavigationController)?.topViewController as? TableView {
|
||||
c.performSegue(withIdentifier: "showCamera", sender: nil)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Called when the user activates your application by selecting a shortcut on the home screen, except when
|
||||
application(_:,willFinishLaunchingWithOptions:) or application(_:didFinishLaunchingWithOptions) returns `false`.
|
||||
You should handle the shortcut in those callbacks and return `false` if possible. In that case, this
|
||||
callback is used if your application is already launched in the background.
|
||||
*/
|
||||
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
||||
let handledShortCutItem = handleShortCutItem(shortcutItem)
|
||||
completionHandler(handledShortCutItem)
|
||||
}
|
||||
|
||||
var frontmostViewController: UIViewController? {
|
||||
var controller = window?.rootViewController
|
||||
while let presentedViewController = controller?.presentedViewController {
|
||||
@ -34,9 +85,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
return controller
|
||||
}
|
||||
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
|
||||
DropboxController.shared.handle(url: url)
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AppDelegate: Logger {
|
||||
static let logToken = "[AppDelegate]"
|
||||
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.30.1" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="qlf-I7-aOI">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="qlf-I7-aOI">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.19.1"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
@ -20,17 +20,42 @@
|
||||
<color key="backgroundColor" red="0.14168914909999999" green="0.14168914909999999" blue="0.14168914909999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="separatorColor" red="0.4408732927524982" green="0.4408732927524982" blue="0.4408732927524982" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
<sections>
|
||||
<tableViewSection headerTitle="Mosaic" footerTitle="Show a grid view of all caps." id="gNs-aR-ZXg">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="41x-gC-Qt0" style="IBUITableViewCellStyleDefault" id="jeG-JT-PbE">
|
||||
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="jeG-JT-PbE" id="h7b-Hm-qFm">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Show mosaic" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="41x-gC-Qt0">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
<connections>
|
||||
<segue destination="wzG-WL-mtW" kind="show" identifier="showMosaic" id="bU4-Uu-zLI"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Database" footerTitle="New caps can't be found through matching. Update the app to the newest version to receive new matching models." id="t1t-6Z-uZp">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="xVf-9U-8dt" detailTextLabel="cwp-eR-aXo" style="IBUITableViewCellStyleSubtitle" id="pQw-5h-loP">
|
||||
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="175" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pQw-5h-loP" id="Hkl-nU-jPG">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="1167 caps, 5600 images" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="xVf-9U-8dt">
|
||||
<rect key="frame" x="16" y="5" width="185.5" height="20.5"/>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="1167 caps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="xVf-9U-8dt">
|
||||
<rect key="frame" x="16" y="5" width="76" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
@ -47,12 +72,33 @@
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="NAMES" footerTitle="Updates names and image counts of the index" id="fnu-vJ-6gE">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="og4-Az-LlV" detailTextLabel="wCH-gF-GY2" style="IBUITableViewCellStyleSubtitle" id="5k2-RN-OpJ">
|
||||
<rect key="frame" x="0.0" y="219" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5k2-RN-OpJ" id="l2t-Ds-k5L">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="5600 images" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="og4-Az-LlV">
|
||||
<rect key="frame" x="16" y="5" width="101" height="20.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="6.15 images/cap, lowest count: 4 (36x)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="wCH-gF-GY2">
|
||||
<rect key="frame" x="16" y="25.5" width="219.5" height="14.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="NLm-th-Ww3" detailTextLabel="Zdl-lL-KYt" style="IBUITableViewCellStyleSubtitle" id="M1J-fv-ztL">
|
||||
<rect key="frame" x="0.0" y="207" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="263" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="M1J-fv-ztL" id="r9E-Qt-wSx">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
@ -78,10 +124,68 @@
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Refresh" footerTitle="Refreshes the image count of all caps. This will take a while." id="3rn-AC-q60">
|
||||
<tableViewSection headerTitle="Classifier" footerTitle="Choose which of the available classifiers should be used for image comparison." id="wUs-7C-Kzz">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="U7G-Xu-7Xd" style="IBUITableViewCellStyleDefault" id="h7a-jf-vVw">
|
||||
<rect key="frame" x="0.0" y="414.5" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="h7a-jf-vVw" id="ZoJ-cw-jsL">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Squeezenet" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="U7G-Xu-7Xd">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="IQ2-te-8MM" style="IBUITableViewCellStyleDefault" id="EBb-eO-HMg">
|
||||
<rect key="frame" x="0.0" y="458.5" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="EBb-eO-HMg" id="zG0-L8-y2b">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Resnet" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="IQ2-te-8MM">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="voV-Qq-csv" style="IBUITableViewCellStyleDefault" id="NLd-me-0rl">
|
||||
<rect key="frame" x="0.0" y="502.5" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="NLd-me-0rl" id="ZNG-Cc-N8D">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Xcode" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="voV-Qq-csv">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Refresh" footerTitle="Update ressources that might be outdated." id="3rn-AC-q60">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="tgU-ma-Xhz" style="IBUITableViewCellStyleDefault" id="GoI-GJ-dx1">
|
||||
<rect key="frame" x="0.0" y="326.5" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="638" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="GoI-GJ-dx1" id="AOV-5g-KkH">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
@ -98,12 +202,30 @@
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="NgT-am-9HS" style="IBUITableViewCellStyleDefault" id="d8A-F9-8yL">
|
||||
<rect key="frame" x="0.0" y="682" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="d8A-F9-8yL" id="aA7-ai-YNA">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Update thumbnails" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="NgT-am-9HS">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Dropbox" footerTitle="Sign in to dropbox to access the cap database." id="Nw5-uf-OcQ">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="dW2-Yd-L4J" style="IBUITableViewCellStyleDefault" id="Hie-e3-jja">
|
||||
<rect key="frame" x="0.0" y="462" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="801.5" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Hie-e3-jja" id="z9u-9f-fqP">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
|
||||
@ -129,10 +251,13 @@
|
||||
</connections>
|
||||
</tableView>
|
||||
<connections>
|
||||
<outlet property="countsLabel" destination="tgU-ma-Xhz" id="ztT-tr-bKu"/>
|
||||
<outlet property="databaseUpdatesLabel" destination="Zdl-lL-KYt" id="akt-Mb-Aqe"/>
|
||||
<outlet property="dropboxAccountLabel" destination="dW2-Yd-L4J" id="lIc-yn-YxK"/>
|
||||
<outlet property="imagesStatsLabel" destination="wCH-gF-GY2" id="p5q-E4-hcp"/>
|
||||
<outlet property="recognizedCapsLabel" destination="cwp-eR-aXo" id="Nvh-8s-sRU"/>
|
||||
<outlet property="totalCapsLabel" destination="xVf-9U-8dt" id="LeK-Xt-loM"/>
|
||||
<outlet property="totalImagesLabel" destination="og4-Az-LlV" id="sVW-t6-1MF"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="rhf-CV-Ipz" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
@ -221,7 +346,7 @@
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="xnW-hF-IOj" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="4097" y="2262"/>
|
||||
<point key="canvasLocation" x="2740" y="2262"/>
|
||||
</scene>
|
||||
<!--New image-->
|
||||
<scene sceneID="5iQ-Nr-LFc">
|
||||
@ -352,6 +477,157 @@
|
||||
</objects>
|
||||
<point key="canvasLocation" x="4096.8000000000002" y="1557.5712143928038"/>
|
||||
</scene>
|
||||
<!--Grid View Controller-->
|
||||
<scene sceneID="Hjr-cD-s35">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="GridView" id="wzG-WL-mtW" customClass="GridViewController" customModule="CapCollector" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="9sc-l8-fcK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TCx-cV-mMG">
|
||||
<rect key="frame" x="0.0" y="64" width="375" height="603"/>
|
||||
</scrollView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.14168914909999999" green="0.14168914909999999" blue="0.14168914909999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="TCx-cV-mMG" firstAttribute="top" secondItem="WAE-if-wuA" secondAttribute="top" id="C5g-UD-eI2"/>
|
||||
<constraint firstItem="WAE-if-wuA" firstAttribute="bottom" secondItem="TCx-cV-mMG" secondAttribute="bottom" id="J5D-Xs-Unl"/>
|
||||
<constraint firstItem="TCx-cV-mMG" firstAttribute="leading" secondItem="WAE-if-wuA" secondAttribute="leading" id="fXw-7M-B0G"/>
|
||||
<constraint firstItem="WAE-if-wuA" firstAttribute="trailing" secondItem="TCx-cV-mMG" secondAttribute="trailing" id="sY8-ka-loB"/>
|
||||
</constraints>
|
||||
<viewLayoutGuide key="safeArea" id="WAE-if-wuA"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="scrollView" destination="TCx-cV-mMG" id="Isn-jV-DBf"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="9Os-vj-mkT" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="4096.8000000000002" y="2261.9190404797605"/>
|
||||
</scene>
|
||||
<!--Sort Controller-->
|
||||
<scene sceneID="fFw-OX-Mag">
|
||||
<objects>
|
||||
<tableViewController storyboardIdentifier="SortController" id="xVJ-JZ-U8g" customClass="SortController" customModule="CapCollector" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" scrollEnabled="NO" dataMode="static" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="LcF-G6-Sln">
|
||||
<rect key="frame" x="0.0" y="0.0" width="150" height="310"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.14168914909999999" green="0.14168914909999999" blue="0.14168914909999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="separatorColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<sections>
|
||||
<tableViewSection headerTitle="Sort order" id="XuT-wG-qHV">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" textLabel="2cU-Pz-MYZ" rowHeight="40" style="IBUITableViewCellStyleDefault" id="vYb-0s-NQp">
|
||||
<rect key="frame" x="0.0" y="55.5" width="150" height="40"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="vYb-0s-NQp" id="8bD-vd-HWF">
|
||||
<rect key="frame" x="0.0" y="0.0" width="110" height="39.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Ascending" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="2cU-Pz-MYZ">
|
||||
<rect key="frame" x="16" y="0.0" width="94" height="39.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Sort by" id="a86-13-jdq">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" textLabel="jBZ-Qh-cQp" rowHeight="40" style="IBUITableViewCellStyleDefault" id="RIw-UJ-LAh">
|
||||
<rect key="frame" x="0.0" y="151.5" width="150" height="40"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="RIw-UJ-LAh" id="84U-pf-v8B">
|
||||
<rect key="frame" x="0.0" y="0.0" width="110" height="39.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Id" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="jBZ-Qh-cQp">
|
||||
<rect key="frame" x="16" y="0.0" width="94" height="39.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="Vbx-7v-vhu" rowHeight="40" style="IBUITableViewCellStyleDefault" id="V6N-Bw-TfC">
|
||||
<rect key="frame" x="0.0" y="191.5" width="150" height="40"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="V6N-Bw-TfC" id="kjo-xX-12y">
|
||||
<rect key="frame" x="0.0" y="0.0" width="150" height="39.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Vbx-7v-vhu">
|
||||
<rect key="frame" x="16" y="0.0" width="118" height="39.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="neG-u0-8YU" rowHeight="40" style="IBUITableViewCellStyleDefault" id="YR5-96-lup">
|
||||
<rect key="frame" x="0.0" y="231.5" width="150" height="40"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="YR5-96-lup" id="Bae-Lt-m8T">
|
||||
<rect key="frame" x="0.0" y="0.0" width="150" height="39.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Image count" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="neG-u0-8YU">
|
||||
<rect key="frame" x="16" y="0.0" width="118" height="39.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" textLabel="hUh-3J-pzj" rowHeight="40" style="IBUITableViewCellStyleDefault" id="3yB-nf-zbV">
|
||||
<rect key="frame" x="0.0" y="271.5" width="150" height="40"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3yB-nf-zbV" id="xiR-G2-PuL">
|
||||
<rect key="frame" x="0.0" y="0.0" width="150" height="39.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Match" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="hUh-3J-pzj">
|
||||
<rect key="frame" x="16" y="0.0" width="118" height="39.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.20027729420000001" green="0.19907802899999999" blue="0.2014765594" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="xVJ-JZ-U8g" id="27V-wO-lvY"/>
|
||||
<outlet property="delegate" destination="xVJ-JZ-U8g" id="TdR-BV-8pz"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<nil key="simulatedTopBarMetrics"/>
|
||||
<nil key="simulatedBottomBarMetrics"/>
|
||||
<size key="freeformSize" width="150" height="310"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="l35-YK-LmX" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3589" y="1052"/>
|
||||
</scene>
|
||||
<!--Caps-->
|
||||
<scene sceneID="3Dy-nA-zIw">
|
||||
<objects>
|
||||
@ -360,37 +636,47 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsVerticalScrollIndicator="NO" keyboardDismissMode="onDrag" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="Djy-MM-jNn">
|
||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsVerticalScrollIndicator="NO" keyboardDismissMode="onDrag" dataMode="prototypes" style="plain" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="Djy-MM-jNn">
|
||||
<rect key="frame" x="0.0" y="154" width="375" height="457"/>
|
||||
<color key="backgroundColor" red="0.14168914909999999" green="0.14168914909999999" blue="0.14168914909999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="separatorColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="cap" rowHeight="100" id="ubX-SO-ltt" customClass="CapCell" customModule="CapCollector" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="28" width="375" height="100"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ubX-SO-ltt" id="BD9-Ic-hwS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="100"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="99.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="eHr-Uk-WC9" customClass="RoundedImageView" customModule="CapCollector" customModuleProvider="target">
|
||||
<rect key="frame" x="2" y="2" width="96" height="96"/>
|
||||
<rect key="frame" x="2" y="2" width="95.5" height="95.5"/>
|
||||
<color key="tintColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="eHr-Uk-WC9" secondAttribute="height" multiplier="1:1" id="640-1g-g0v"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="color" keyPath="borderColor">
|
||||
<color key="value" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
|
||||
<real key="value" value="1"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="71 % match" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Pe1-sR-2RT">
|
||||
<rect key="frame" x="106" y="2.5" width="128" height="21"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="# images" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Pe1-sR-2RT">
|
||||
<rect key="frame" x="105.5" y="10" width="70" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Some brand with a long name of more than 2 lines" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Sij-H0-zYC">
|
||||
<rect key="frame" x="106" y="25.5" width="261" height="49"/>
|
||||
<rect key="frame" x="105.5" y="33" width="261.5" height="49"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
|
||||
<color key="textColor" red="0.83921568629999999" green="0.9379758883" blue="0.54901960780000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="# caps" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nJ1-TQ-ZT3">
|
||||
<rect key="frame" x="315.5" y="2.5" width="51.5" height="21"/>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="71 % match" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nJ1-TQ-ZT3">
|
||||
<rect key="frame" x="279" y="10" width="88" height="21"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
@ -398,24 +684,23 @@
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="Pe1-sR-2RT" firstAttribute="leading" secondItem="eHr-Uk-WC9" secondAttribute="trailing" constant="8" id="1bU-zq-83L"/>
|
||||
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="Sij-H0-zYC" secondAttribute="bottom" constant="8" id="3e4-bs-Jka"/>
|
||||
<constraint firstItem="eHr-Uk-WC9" firstAttribute="leading" secondItem="BD9-Ic-hwS" secondAttribute="leading" constant="2" id="BH6-4m-jIT"/>
|
||||
<constraint firstItem="Sij-H0-zYC" firstAttribute="top" secondItem="Pe1-sR-2RT" secondAttribute="bottom" constant="2" id="FNF-xm-BjR"/>
|
||||
<constraint firstItem="Pe1-sR-2RT" firstAttribute="top" secondItem="BD9-Ic-hwS" secondAttribute="top" constant="10" id="KY6-3Z-p5Q"/>
|
||||
<constraint firstItem="Sij-H0-zYC" firstAttribute="leading" secondItem="Pe1-sR-2RT" secondAttribute="leading" id="SdR-AF-gcG"/>
|
||||
<constraint firstAttribute="bottom" secondItem="eHr-Uk-WC9" secondAttribute="bottom" constant="2" id="UGo-B1-mHf"/>
|
||||
<constraint firstItem="nJ1-TQ-ZT3" firstAttribute="leading" secondItem="Pe1-sR-2RT" secondAttribute="trailing" constant="81.5" id="WQ3-ZG-Hva"/>
|
||||
<constraint firstItem="nJ1-TQ-ZT3" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Pe1-sR-2RT" secondAttribute="trailing" constant="10" id="WQ3-ZG-Hva"/>
|
||||
<constraint firstItem="nJ1-TQ-ZT3" firstAttribute="top" secondItem="Pe1-sR-2RT" secondAttribute="top" id="WcF-d6-GHl"/>
|
||||
<constraint firstItem="Sij-H0-zYC" firstAttribute="top" secondItem="Pe1-sR-2RT" secondAttribute="bottom" constant="2" id="cCu-FA-TJ5"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Sij-H0-zYC" secondAttribute="trailing" constant="8" id="hKh-2e-iDu"/>
|
||||
<constraint firstItem="eHr-Uk-WC9" firstAttribute="centerY" secondItem="BD9-Ic-hwS" secondAttribute="centerY" id="lRZ-Qc-0MI"/>
|
||||
<constraint firstItem="Sij-H0-zYC" firstAttribute="centerY" secondItem="BD9-Ic-hwS" secondAttribute="centerY" id="mIz-nP-Mtz"/>
|
||||
<constraint firstItem="nJ1-TQ-ZT3" firstAttribute="trailing" secondItem="Sij-H0-zYC" secondAttribute="trailing" id="r1z-vT-brQ"/>
|
||||
<constraint firstItem="nJ1-TQ-ZT3" firstAttribute="centerY" secondItem="Pe1-sR-2RT" secondAttribute="centerY" id="xBh-YR-DhC"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.14168914909999999" green="0.14168914909999999" blue="0.14168914909999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<connections>
|
||||
<outlet property="capImage" destination="eHr-Uk-WC9" id="sQH-w5-lp6"/>
|
||||
<outlet property="countLabel" destination="nJ1-TQ-ZT3" id="Vde-d4-EhH"/>
|
||||
<outlet property="matchLabel" destination="Pe1-sR-2RT" id="ZPR-ro-2Yg"/>
|
||||
<outlet property="countLabel" destination="Pe1-sR-2RT" id="ODc-sB-fNO"/>
|
||||
<outlet property="matchLabel" destination="nJ1-TQ-ZT3" id="M8b-pT-ano"/>
|
||||
<outlet property="nameLabel" destination="Sij-H0-zYC" id="Wc1-h9-i2E"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
@ -451,6 +736,14 @@
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="TLY-ak-c4W" secondAttribute="height" multiplier="1:1" id="SU9-Ph-0PV"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="color" keyPath="borderColor">
|
||||
<color key="value" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
|
||||
<real key="value" value="1"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</imageView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Wyc-2T-CIk" customClass="RoundedButton" customModule="CapCollector" customModuleProvider="target">
|
||||
<rect key="frame" x="96" y="30" width="131.5" height="30"/>
|
||||
@ -529,6 +822,11 @@
|
||||
<segue destination="x62-XO-Rsu" kind="show" identifier="showSettings" id="TWW-Vh-GTa"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" title="Sort" id="Bii-kx-Exm">
|
||||
<connections>
|
||||
<action selector="showSortOptions:" destination="VSb-c5-JF6" id="pzj-fP-M6S"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<nil key="simulatedBottomBarMetrics"/>
|
||||
<connections>
|
||||
@ -551,7 +849,7 @@
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="cDZ-9F-oGg">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="qlf-I7-aOI" sceneMemberID="viewController">
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="qlf-I7-aOI" customClass="NavigationController" customModule="CapCollector" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" id="tO3-6d-IEo">
|
||||
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
|
||||
|
@ -17,7 +17,7 @@ protocol CameraControllerDelegate {
|
||||
|
||||
class CameraController: UIViewController {
|
||||
|
||||
private let imageSize = 299 // New for XCode models, 227 for turicreate
|
||||
private let imageSize = 299 // New for XCode models, 227/229 for turicreate
|
||||
|
||||
// MARK: Outlets
|
||||
|
||||
@ -35,16 +35,29 @@ class CameraController: UIViewController {
|
||||
|
||||
var delegate: CameraControllerDelegate?
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
override var shouldAutorotate: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@IBAction func backButtonPressed() {
|
||||
self.giveFeedback(.medium)
|
||||
delegate?.didCancel()
|
||||
self.dismiss(animated: true)
|
||||
}
|
||||
|
||||
@IBAction func imageButtonPressed() {
|
||||
self.giveFeedback(.medium)
|
||||
imageButton.isEnabled = false
|
||||
event("Taking image")
|
||||
cameraView.capture()
|
||||
}
|
||||
|
||||
@ -84,6 +97,11 @@ class CameraController: UIViewController {
|
||||
cancelButton.set(template: "cancel", with: tint)
|
||||
}
|
||||
|
||||
private func giveFeedback(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
|
||||
let generator = UIImpactFeedbackGenerator(style: style)
|
||||
generator.impactOccurred()
|
||||
}
|
||||
|
||||
// MARK: Alerts
|
||||
|
||||
private func showNoCameraAccessAlert() {
|
||||
@ -105,6 +123,10 @@ class CameraController: UIViewController {
|
||||
alert.addAction(settingsAction)
|
||||
self.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||
cameraView.didReceiveTouch(touches.first!)
|
||||
}
|
||||
}
|
||||
|
||||
extension CameraController: PhotoCaptureHandlerDelegate {
|
||||
|
@ -52,6 +52,8 @@ class CameraView: UIView {
|
||||
|
||||
private let photoOutput = AVCapturePhotoOutput()
|
||||
|
||||
private var cameraDevice: AVCaptureDevice?
|
||||
|
||||
private let photoCaptureProcessor = PhotoCaptureHandler()
|
||||
|
||||
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
|
||||
@ -146,52 +148,80 @@ class CameraView: UIView {
|
||||
session.sessionPreset = .photo
|
||||
|
||||
// Add video input.
|
||||
do {
|
||||
|
||||
guard let backCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
|
||||
print("No camera on device")
|
||||
error("No camera on device")
|
||||
setupResult = .configurationFailed
|
||||
session.commitConfiguration()
|
||||
return
|
||||
}
|
||||
|
||||
let videoDeviceInput = try AVCaptureDeviceInput(device: backCameraDevice)
|
||||
let videoDeviceInput: AVCaptureDeviceInput
|
||||
do {
|
||||
videoDeviceInput = try AVCaptureDeviceInput(device: backCameraDevice)
|
||||
} catch {
|
||||
self.error("Could not create video device input: \(error)")
|
||||
setupResult = .configurationFailed
|
||||
session.commitConfiguration()
|
||||
return
|
||||
}
|
||||
|
||||
guard session.canAddInput(videoDeviceInput) else {
|
||||
error("Could not add video device input to the session")
|
||||
setupResult = .configurationFailed
|
||||
session.commitConfiguration()
|
||||
return
|
||||
}
|
||||
|
||||
if session.canAddInput(videoDeviceInput) {
|
||||
session.addInput(videoDeviceInput)
|
||||
self.videoDeviceInput = videoDeviceInput
|
||||
|
||||
self.cameraDevice = backCameraDevice
|
||||
DispatchQueue.main.async {
|
||||
self.videoPreviewLayer.connection?.videoOrientation = .portrait
|
||||
}
|
||||
} else {
|
||||
print("Could not add video device input to the session")
|
||||
setupResult = .configurationFailed
|
||||
session.commitConfiguration()
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
print("Could not create video device input: \(error)")
|
||||
setupResult = .configurationFailed
|
||||
session.commitConfiguration()
|
||||
return
|
||||
}
|
||||
|
||||
// Add photo output.
|
||||
if session.canAddOutput(photoOutput) {
|
||||
guard session.canAddOutput(photoOutput) else {
|
||||
error("Could not add photo output to the session")
|
||||
setupResult = .configurationFailed
|
||||
session.commitConfiguration()
|
||||
return
|
||||
}
|
||||
session.addOutput(photoOutput)
|
||||
|
||||
photoOutput.isHighResolutionCaptureEnabled = true
|
||||
photoOutput.isDepthDataDeliveryEnabled = false
|
||||
photoOutput.isDualCameraDualPhotoDeliveryEnabled = false
|
||||
photoOutput.isLivePhotoCaptureEnabled = false
|
||||
} else {
|
||||
print("Could not add photo output to the session")
|
||||
setupResult = .configurationFailed
|
||||
session.commitConfiguration()
|
||||
return
|
||||
}
|
||||
|
||||
session.commitConfiguration()
|
||||
func didReceiveTouch(_ touch: UITouch) {
|
||||
let screenSize = bounds.size
|
||||
let location = touch.location(in: self)
|
||||
let focusPoint = CGPoint(x: location.y / screenSize.height, y: 1.0 - location.x / screenSize.width)
|
||||
event("Focusing on point (\(focusPoint.x),\(focusPoint.y))")
|
||||
if let device = cameraDevice {
|
||||
do {
|
||||
try device.lockForConfiguration()
|
||||
|
||||
if device.isFocusPointOfInterestSupported {
|
||||
device.focusPointOfInterest = focusPoint
|
||||
device.focusMode = .autoFocus
|
||||
}
|
||||
// if device.isExposurePointOfInterestSupported {
|
||||
// device.exposurePointOfInterest = focusPoint
|
||||
// device.exposureMode = .autoExpose
|
||||
// }
|
||||
device.unlockForConfiguration()
|
||||
} catch {
|
||||
self.error("Could not lock device for configuration: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CameraView: Logger {
|
||||
|
||||
static let logToken = "CameraView"
|
||||
}
|
||||
|
201
CapCollector/Classifier.swift
Normal file
201
CapCollector/Classifier.swift
Normal file
@ -0,0 +1,201 @@
|
||||
//
|
||||
// VisionHandler.swift
|
||||
// CapFinder
|
||||
//
|
||||
// Created by User on 12.02.18.
|
||||
// Copyright © 2018 User. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Vision
|
||||
import CoreML
|
||||
import UIKit
|
||||
|
||||
/// Notify the delegate about
|
||||
protocol ClassifierDelegate {
|
||||
|
||||
/// Features found
|
||||
func classifier(finished image: UIImage?)
|
||||
|
||||
/// Error handler
|
||||
func classifier(error: String)
|
||||
}
|
||||
|
||||
/// Recognise categories in images
|
||||
class Classifier: Logger {
|
||||
|
||||
static let logToken = "[Classifier]"
|
||||
|
||||
static var shared = Classifier()
|
||||
|
||||
/// Handles errors and recognised features
|
||||
var delegate: ClassifierDelegate?
|
||||
|
||||
// MARK: Stored predictions
|
||||
|
||||
private var predictions = [[Int : Float]]()
|
||||
|
||||
private var notify = false
|
||||
|
||||
private var image: UIImage?
|
||||
|
||||
private func request(for model: MLModel, name: String) -> VNCoreMLRequest {
|
||||
|
||||
let model = try! VNCoreMLModel(for: model)
|
||||
|
||||
let request = VNCoreMLRequest(model: model, completionHandler: { [weak self] request, error in
|
||||
self?.process(request: request, error: error)
|
||||
self?.event("Finished \(name) prediction (\(self!.predictions.count)/\(self!.requestCount))")
|
||||
})
|
||||
request.imageCropAndScaleOption = .centerCrop
|
||||
return request
|
||||
}
|
||||
|
||||
private var requestCount = 0
|
||||
|
||||
private var requests: [VNCoreMLRequest] {
|
||||
var reqs = [VNCoreMLRequest]()
|
||||
if Persistence.squeezenet {
|
||||
reqs.append(request(for: Squeezenet().model, name: "Squeezenet"))
|
||||
}
|
||||
if Persistence.resnet {
|
||||
reqs.append(request(for: Resnet().model, name: "Resnet"))
|
||||
}
|
||||
if Persistence.xcode {
|
||||
reqs.append(request(for: ImageClassifier().model, name: "Xcode"))
|
||||
}
|
||||
requestCount = reqs.count
|
||||
return reqs
|
||||
}
|
||||
|
||||
/**
|
||||
Classify an image
|
||||
- parameter image: The image to classify
|
||||
- parameter reportingImage: Set to true, if the delegate should receive the image
|
||||
*/
|
||||
func recognise(image: UIImage, reportingImage: Bool = true) {
|
||||
predictions.removeAll()
|
||||
self.image = image
|
||||
notify = reportingImage
|
||||
performClassifications()
|
||||
}
|
||||
|
||||
private func performClassifications() {
|
||||
let orientation = CGImagePropertyOrientation(image!.imageOrientation)
|
||||
guard let ciImage = CIImage(image: image!) else {
|
||||
report(error: "Unable to create CIImage")
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
|
||||
let requests = self.requests
|
||||
guard requests.count > 0 else {
|
||||
self.report(error: "No classifiers selected")
|
||||
return
|
||||
}
|
||||
do {
|
||||
try handler.perform(requests)
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
self.report(error: "Classification failed: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func process(request: VNRequest, error: Error?) {
|
||||
guard let result = request.results as? [VNClassificationObservation],
|
||||
result.isEmpty == false else {
|
||||
report(error: "Unable to classify image: \(error?.localizedDescription ?? "No error thrown")")
|
||||
return
|
||||
}
|
||||
let current = dict(from: result)
|
||||
predictions.append(current)
|
||||
|
||||
if predictions.count == requestCount {
|
||||
updateRecognizedCapsCount()
|
||||
combine()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a dictionary from a vision prediction
|
||||
private func dict(from results: [VNClassificationObservation]) -> [Int : Float] {
|
||||
let array = results.map{ item -> (Int, Float) in
|
||||
return (Int(item.identifier) ?? 0, item.confidence)
|
||||
}
|
||||
return [Int : Float](uniqueKeysWithValues: array)
|
||||
}
|
||||
|
||||
/// Combine two predictions
|
||||
private func combine() {
|
||||
Cap.unsortedCaps.forEach { cap in
|
||||
var result: Float = 0
|
||||
for index in 0..<predictions.count {
|
||||
result = max(predictions[index][cap.id] ?? 0, result)
|
||||
}
|
||||
cap.match = result
|
||||
}
|
||||
Cap.hasMatches = true
|
||||
report()
|
||||
}
|
||||
|
||||
private func updateRecognizedCapsCount() {
|
||||
let recognizedCaps = predictions.map { prediction in
|
||||
return prediction.count
|
||||
}
|
||||
Persistence.recognizedCapCount = recognizedCaps.max()!
|
||||
}
|
||||
|
||||
// MARK: Callbacks
|
||||
|
||||
private func cleanup() {
|
||||
predictions.removeAll()
|
||||
image = nil
|
||||
}
|
||||
|
||||
private func report(error message: String) {
|
||||
guard delegate != nil else {
|
||||
error("No delegate: " + message)
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.cleanup()
|
||||
self.delegate?.classifier(error: message)
|
||||
}
|
||||
}
|
||||
|
||||
private func report() {
|
||||
guard delegate != nil else {
|
||||
error("No delegate")
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
let img = self.notify ? self.image : nil
|
||||
self.cleanup()
|
||||
self.delegate?.classifier(finished: img)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CGImagePropertyOrientation {
|
||||
/**
|
||||
Converts a `UIImageOrientation` to a corresponding
|
||||
`CGImagePropertyOrientation`. The cases for each
|
||||
orientation are represented by different raw values.
|
||||
|
||||
- Tag: ConvertOrientation
|
||||
*/
|
||||
init(_ orientation: UIImage.Orientation) {
|
||||
switch orientation {
|
||||
case .up: self = .up
|
||||
case .upMirrored: self = .upMirrored
|
||||
case .down: self = .down
|
||||
case .downMirrored: self = .downMirrored
|
||||
case .left: self = .left
|
||||
case .leftMirrored: self = .leftMirrored
|
||||
case .right: self = .right
|
||||
case .rightMirrored: self = .rightMirrored
|
||||
}
|
||||
}
|
||||
}
|
746
CapCollector/Data/Cap.swift
Normal file
746
CapCollector/Data/Cap.swift
Normal file
@ -0,0 +1,746 @@
|
||||
//
|
||||
// Cap.swift
|
||||
// CapCollector
|
||||
//
|
||||
// Created by Christoph on 19.11.18.
|
||||
// Copyright © 2018 CH. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftyDropbox
|
||||
|
||||
protocol CapsDelegate: class {
|
||||
|
||||
func capHasUpdates(_ cap: Cap)
|
||||
|
||||
func capsLoaded()
|
||||
}
|
||||
|
||||
final class Cap {
|
||||
|
||||
// MARK: - Static variables
|
||||
|
||||
static let jpgQuality: CGFloat = 0.3
|
||||
|
||||
private static let mosaicColumns = 40
|
||||
|
||||
static let mosaicCellSize: CGFloat = 60
|
||||
|
||||
private static let mosaicRowHeight = mosaicCellSize * 0.866
|
||||
|
||||
private static let mosaicMargin = mosaicCellSize - mosaicRowHeight
|
||||
|
||||
static var delegate: CapsDelegate?
|
||||
|
||||
static var shouldSave = true {
|
||||
didSet {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
static var hasMatches = false {
|
||||
didSet {
|
||||
guard !hasMatches else { return }
|
||||
all.forEach { _, cap in
|
||||
cap.match = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var nextUnusedId: Int {
|
||||
return (all.keys.max() ?? 0) + 1
|
||||
}
|
||||
|
||||
/// The number of caps currently in the database
|
||||
static var totalCapCount: Int {
|
||||
return all.count
|
||||
}
|
||||
|
||||
/// The total number of images for all caps
|
||||
static var imageCount: Int {
|
||||
return all.reduce(0) { sum, cap in
|
||||
return sum + cap.value.count
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Match all cap names against the given string and return matches.
|
||||
- note: Each space-separated part of the string is matched individually
|
||||
*/
|
||||
static func caps(matching text: String) -> [Cap] {
|
||||
let cleaned = text.clean
|
||||
let found = all.compactMap { (_,cap) -> Cap? in
|
||||
// For each part of text, check if name contains it
|
||||
for textItem in cleaned.components(separatedBy: " ") {
|
||||
if textItem != "" && !cap.name.contains(textItem) { return nil }
|
||||
}
|
||||
return cap
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
// MARK: - Variables
|
||||
|
||||
/// The unique number of the cap
|
||||
let id: Int
|
||||
|
||||
/// The tile position of the cap
|
||||
var tile: Int
|
||||
|
||||
/// The name of the cap
|
||||
var name: String {
|
||||
didSet {
|
||||
cleanName = name.clean
|
||||
Cap.save()
|
||||
event("Updated name for cap \(id) to \(name)")
|
||||
Cap.delegate?.capHasUpdates(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The name of the cap wothout special characters
|
||||
private(set) var cleanName: String
|
||||
|
||||
/// The number of images existing for the cap
|
||||
private(set) var count: Int {
|
||||
didSet {
|
||||
Cap.save()
|
||||
event("Updated count for cap \(id) to \(count)")
|
||||
Cap.delegate?.capHasUpdates(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The similarity of the cap to the currently processed image
|
||||
var match: Float? = nil
|
||||
|
||||
// MARK: - All caps
|
||||
|
||||
/// A dictionary of all known caps
|
||||
static var all = [Int : Cap]()
|
||||
|
||||
// MARK: - Tile information
|
||||
|
||||
/// A dictionary of the caps for the tiles
|
||||
static var tiles = [Int : Cap]()
|
||||
|
||||
/**
|
||||
Get the cap image for a tile.
|
||||
*/
|
||||
static func tileImage(tile: Int) -> UIImage? {
|
||||
return tiles[tile]?.thumbnail
|
||||
}
|
||||
|
||||
/**
|
||||
Switch two tiles.
|
||||
*/
|
||||
static func switchTiles(_ lhs: Int, _ rhs: Int) {
|
||||
let l = tiles[lhs]!
|
||||
let r = tiles[rhs]!
|
||||
l.tile = rhs
|
||||
r.tile = lhs
|
||||
tiles[rhs] = l
|
||||
tiles[lhs] = r
|
||||
event("Switched tiles \(lhs) and \(rhs)")
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/**
|
||||
Create a new cap with an image
|
||||
- parameter image: The main image of the cap
|
||||
- parameter name: The name of the cap
|
||||
*/
|
||||
init?(image: UIImage, name: String) {
|
||||
self.id = Cap.nextUnusedId
|
||||
self.tile = id - 1
|
||||
self.name = name
|
||||
self.count = 1
|
||||
self.cleanName = name.clean
|
||||
guard save(mainImage: image) else {
|
||||
return nil
|
||||
}
|
||||
upload(mainImage: image) { success in
|
||||
guard success else { return }
|
||||
Cap.all[self.id] = self
|
||||
Cap.tiles[self.id] = self
|
||||
Cap.save()
|
||||
Cap.updateMosaicWithNewCap(id: self.id, image)
|
||||
Cap.delegate?.capHasUpdates(self)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Create a cap from a line in the cap list file
|
||||
*/
|
||||
init?(line: String) {
|
||||
guard line != "" else {
|
||||
return nil
|
||||
}
|
||||
let parts = line.components(separatedBy: ";")
|
||||
guard parts.count == 4 else {
|
||||
Cap.error("Cap names: Invalid line \(line)")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let nr = Int(parts[0]) else {
|
||||
Cap.error("Invalid id in line \(line)")
|
||||
return nil
|
||||
}
|
||||
guard let count = Int(parts[2]) else {
|
||||
Cap.error("Invalid count in line \(line)")
|
||||
return nil
|
||||
}
|
||||
guard let tile = Int(parts[3]) else {
|
||||
Cap.error("Invalid tile in line \(line)")
|
||||
return nil
|
||||
}
|
||||
self.id = nr
|
||||
self.name = parts[1]
|
||||
self.count = count
|
||||
self.cleanName = name.clean
|
||||
self.tile = tile
|
||||
Cap.tiles[tile] = self
|
||||
Cap.all[id] = self
|
||||
}
|
||||
|
||||
// MARK: - Images
|
||||
|
||||
/// The main image of the cap
|
||||
var image: UIImage? {
|
||||
guard let data = DiskManager.image(for: id) else {
|
||||
return nil
|
||||
}
|
||||
return UIImage(data: data)
|
||||
}
|
||||
|
||||
/// The main image of the cap
|
||||
var thumbnail: UIImage? {
|
||||
if let data = DiskManager.thumbnail(for: id) {
|
||||
return UIImage(data: data)
|
||||
}
|
||||
return makeThumbnail()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func makeThumbnail() -> UIImage? {
|
||||
guard let img = image else {
|
||||
return nil
|
||||
}
|
||||
let len = GridViewController.len * 2
|
||||
let thumb = img.resize(to: CGSize.init(width: len, height: len))
|
||||
guard let data = thumb.pngData() else {
|
||||
error("Failed to get PNG data from thumbnail for cap \(id)")
|
||||
return nil
|
||||
}
|
||||
_ = DiskManager.save(thumbnailData: data, for: id)
|
||||
event("Created thumbnail for cap \(id)")
|
||||
return thumb
|
||||
}
|
||||
|
||||
/**
|
||||
Download a specified image of the cap.
|
||||
- Note: If the downloaded image is the main image, it is automatically saved to disk
|
||||
- Note: If the main image is requested and already downloaded, it is returned directly
|
||||
- parameter number: The number of the image
|
||||
- parameter completion: The completion handler, called with the image if successful
|
||||
- parameter image: The image, if the download was successful, or nil on error
|
||||
*/
|
||||
func downloadImage(_ number: Int = 0, completion: @escaping (_ image: UIImage?) -> Void) {
|
||||
if number == 0, let image = self.image {
|
||||
event("Main image for cap \(id) already downloaded")
|
||||
completion(image)
|
||||
return
|
||||
}
|
||||
let path = "/Images/\(id)/\(id)-\(number).jpg"
|
||||
DropboxController.client.files.download(path: path).response { data, dbError in
|
||||
if let error = dbError {
|
||||
self.error("Failed to download image data (\(number)) for cap \(self.id): \(error)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
guard let d = data?.1 else {
|
||||
self.error("Failed to download image data (\(number)) for cap \(self.id)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let image = UIImage(data: d) else {
|
||||
self.error("Corrupted image data (\(number)) for cap \(self.id)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
if number == 0 {
|
||||
guard self.save(mainImage: image) else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
self.event("Downloaded image data (\(number)) for cap \(self.id)")
|
||||
completion(image)
|
||||
}
|
||||
}
|
||||
|
||||
func save(mainImage: UIImage) -> Bool {
|
||||
guard let data = mainImage.jpegData(compressionQuality: Cap.jpgQuality) else {
|
||||
error("Failed to convert image to data")
|
||||
return false
|
||||
}
|
||||
guard DiskManager.save(imageData: data, for: id) else {
|
||||
error("Failed to save main image for cap \(id)")
|
||||
return false
|
||||
}
|
||||
event("Saved main image for cap \(id) to disk")
|
||||
guard let _ = makeThumbnail() else {
|
||||
return true
|
||||
}
|
||||
|
||||
Cap.delegate?.capHasUpdates(self)
|
||||
return true
|
||||
}
|
||||
|
||||
func add(image: UIImage, completion: @escaping (Bool) -> Void) {
|
||||
upload(image: image) { saved in
|
||||
guard saved else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
// Increment cap count
|
||||
self.count += 1
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image upload
|
||||
|
||||
private func upload(mainImage: UIImage, completion: @escaping (_ success: Bool) -> Void) {
|
||||
self.createFolder { created in
|
||||
guard created else { return }
|
||||
self.upload(image: mainImage, number: 0, savedCallback: completion)
|
||||
}
|
||||
}
|
||||
|
||||
private func folderExists(completion: @escaping (_ exists: Bool?) -> Void) {
|
||||
let path = "/Images"
|
||||
DropboxController.client.files.listFolder(path: path).response { response, error in
|
||||
if let e = error {
|
||||
self.error("Failed to get image folder list: \(e)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let result = response else {
|
||||
self.error("Failed to get image folder list")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let exists = result.entries.contains { $0.name == "\(self.id)" }
|
||||
completion(exists)
|
||||
}
|
||||
}
|
||||
|
||||
private func createFolder(completion: @escaping (_ success: Bool) -> Void) {
|
||||
// Create folder for cap
|
||||
let path = "/Images/\(id)"
|
||||
DropboxController.client.files.createFolderV2(path: path).response { _, error in
|
||||
if let err = error {
|
||||
self.event("Could not create folder for new cap \(self.id): \(err)")
|
||||
completion(false)
|
||||
} else {
|
||||
self.event("Created folder for new cap \(self.id)")
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func upload(image: UIImage, savedCallback: @escaping (Bool) -> Void) {
|
||||
upload(image: image, number: count, savedCallback: savedCallback)
|
||||
}
|
||||
|
||||
private func upload(image: UIImage, number: Int, savedCallback: @escaping (Bool) -> Void) {
|
||||
// Convert to data
|
||||
guard let data = image.jpegData(compressionQuality: Cap.jpgQuality) else {
|
||||
error("Failed to convert image to data")
|
||||
return
|
||||
}
|
||||
let fileName = "\(id)-\(number).jpg"
|
||||
// Save image to upload folder
|
||||
guard let url = DiskManager.saveForUpload(imageData: data, name: fileName) else {
|
||||
error("Could not save image for cap \(id) to upload folder")
|
||||
savedCallback(false)
|
||||
return
|
||||
}
|
||||
event("Saved image \(number) for cap \(id) for upload")
|
||||
savedCallback(true)
|
||||
|
||||
Cap.upload(url: url) { success in
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static func upload(url: URL, completion: @escaping (Bool) -> Void) {
|
||||
let cap = Int(url.lastPathComponent.components(separatedBy: "-").first!)!
|
||||
let path = "/Images/\(cap)/" + url.lastPathComponent
|
||||
|
||||
let data: Data
|
||||
do {
|
||||
data = try Data(contentsOf: url)
|
||||
} catch {
|
||||
self.error("Could not read data from url \(url.path): \(error)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
DropboxController.client.files.upload(path: path, input: data).response { response, error in
|
||||
if let err = error {
|
||||
self.error("Failed to upload file at url: \(url): \(err)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
Cap.event("Uploaded image \(path)")
|
||||
guard DiskManager.removeFromUpload(url: url) else {
|
||||
self.error("Could not delete uploaded image for cap \(cap) at url \(url)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
static func uploadRemainingImages() {
|
||||
guard let list = DiskManager.pendingUploads else {
|
||||
return
|
||||
}
|
||||
guard list.count != 0 else {
|
||||
event("No pending uploads")
|
||||
return
|
||||
}
|
||||
event("\(list.count) image uploads pending")
|
||||
|
||||
for url in list {
|
||||
upload(url: url) { didUpload in
|
||||
// Delete image from disk if uploaded
|
||||
guard didUpload else {
|
||||
self.error("Could not upload image at url \(url)")
|
||||
return
|
||||
}
|
||||
guard DiskManager.removeFromUpload(url: url) else {
|
||||
self.error("Could not delete uploaded image at url \(url)")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Counts
|
||||
|
||||
func updateCount(completion: @escaping (Bool) -> Void) {
|
||||
getImageCount { response in
|
||||
guard let count = response else {
|
||||
self.error("Could not update count for cap \(self.id)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
self.count = count
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
private func getImageCount(completion: @escaping (Int?) -> Void) {
|
||||
DropboxController.client.files.listFolder(path: "/Images/\(id)").response { response, error in
|
||||
if let err = error {
|
||||
self.error("Error getting folder content of cap \(self.id): \(err)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let files = response?.entries else {
|
||||
self.error("No content for folder of cap \(self.id)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
completion(files.count)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sorted caps
|
||||
|
||||
static var unsortedCaps: Set<Cap> {
|
||||
return Set(all.values)
|
||||
}
|
||||
|
||||
static func capList(sortedBy criteria: SortCriteria, ascending: Bool) -> [Cap] {
|
||||
if ascending {
|
||||
return sorted([Cap](all.values), ascendingBy: criteria)
|
||||
} else {
|
||||
return sorted([Cap](all.values), descendingBy: criteria)
|
||||
}
|
||||
}
|
||||
|
||||
private static func sorted(_ list: [Cap], ascendingBy parameter: SortCriteria) -> [Cap] {
|
||||
switch parameter {
|
||||
case .id: return list.sorted { $0.id < $1.id }
|
||||
case .count: return list.sorted { $0.count < $1.count }
|
||||
case .name: return list.sorted { $0.name < $1.name }
|
||||
case .match: return list.sorted { $0.match ?? 0 < $1.match ?? 0 }
|
||||
}
|
||||
}
|
||||
|
||||
private static func sorted(_ list: [Cap], descendingBy parameter: SortCriteria) -> [Cap] {
|
||||
switch parameter {
|
||||
case .id: return list.sorted { $0.id > $1.id }
|
||||
case .count: return list.sorted { $0.count > $1.count }
|
||||
case .name: return list.sorted { $0.name > $1.name }
|
||||
case .match: return list.sorted { $0.match ?? 0 > $1.match ?? 0 }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Loading, Saving & Uploading cap list
|
||||
|
||||
/**
|
||||
Either load the names from disk or download them from dropbox.
|
||||
- parameter completion: The handler that is called with true on success, false on failure
|
||||
*/
|
||||
static func load() {
|
||||
NameFile.makeAvailable { content in
|
||||
guard let lines = content else {
|
||||
return
|
||||
}
|
||||
self.readNames(from: lines)
|
||||
DispatchQueue.main.async {
|
||||
createAndSaveMosaic()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read all caps from the content of a file
|
||||
private static func readNames(from fileContent: String) {
|
||||
let parts = fileContent.components(separatedBy: "\n")
|
||||
for line in parts {
|
||||
_ = Cap(line: line)
|
||||
}
|
||||
event("Loaded \(totalCapCount) caps from file")
|
||||
delegate?.capsLoaded()
|
||||
}
|
||||
|
||||
static func getCapStatistics() -> [Int] {
|
||||
let counts = all.values.map { $0.count }
|
||||
var c = [Int](repeating: 0, count: counts.max()! + 1)
|
||||
counts.forEach { c[$0] += 1 }
|
||||
return c
|
||||
}
|
||||
|
||||
static func save() {
|
||||
guard shouldSave else { return }
|
||||
let content = namesAsString()
|
||||
NameFile.save(names: content)
|
||||
}
|
||||
|
||||
static func saveAndUpload(completion: @escaping (Bool) -> Void) {
|
||||
let content = namesAsString()
|
||||
NameFile.saveAndUpload(names: content) { success in
|
||||
guard success else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
Persistence.lastUploadedCapCount = totalCapCount
|
||||
Persistence.lastUploadedImageCount = imageCount
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
private static func namesAsString() -> String {
|
||||
return capList(sortedBy: .id, ascending: true).reduce("") { $0 + $1.description }
|
||||
}
|
||||
|
||||
// MARK: - GridView
|
||||
|
||||
private static func size(for tiles: Int) -> CGSize {
|
||||
let columns = CGFloat(mosaicColumns)
|
||||
// Add half of a cell due to row shift
|
||||
let width = (columns + 0.5) * mosaicCellSize
|
||||
let rows = (CGFloat(tiles) / columns).rounded(.up)
|
||||
// Add margin because the last row does not overlap
|
||||
let height = rows * mosaicRowHeight + mosaicMargin
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
|
||||
static func origin(for tile: Int) -> CGPoint {
|
||||
let row = tile / mosaicColumns
|
||||
let column = tile - row * mosaicColumns
|
||||
let x = ( CGFloat(column) + (row.isEven ? 0 : 0.5) ) * mosaicCellSize
|
||||
let y = CGFloat(row) * mosaicRowHeight
|
||||
return CGPoint(x: x, y: y)
|
||||
}
|
||||
|
||||
private static func makeTile(_ tile: Int, image: UIImage) -> RoundedImageView {
|
||||
let point = origin(for: tile)
|
||||
let frame = CGRect(origin: point, size: CGSize(width: mosaicCellSize, height: mosaicCellSize))
|
||||
let view = RoundedImageView(frame: frame)
|
||||
view.image = image
|
||||
return view
|
||||
}
|
||||
|
||||
private static func makeMosaicCanvas() -> UIView {
|
||||
let view = UIView(frame: CGRect(origin: .zero, size: size(for: Cap.totalCapCount)))
|
||||
view.backgroundColor = UIColor(red: 36/255, green: 36/255, blue: 36/255, alpha: 1)
|
||||
return view
|
||||
}
|
||||
|
||||
private static func makeMosaic() -> UIImage? {
|
||||
let canvas = makeMosaicCanvas()
|
||||
|
||||
for cap in Cap.all.values {
|
||||
if let img = cap.image {
|
||||
let view = makeTile(cap.id - 1, image: img)
|
||||
canvas.addSubview(view)
|
||||
} else {
|
||||
error("No image for cap \(cap.id)")
|
||||
}
|
||||
}
|
||||
return render(view: canvas)
|
||||
}
|
||||
|
||||
private static func render(view: UIView) -> UIImage? {
|
||||
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0)
|
||||
defer { UIGraphicsEndImageContext() }
|
||||
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
|
||||
return UIGraphicsGetImageFromCurrentImageContext()
|
||||
}
|
||||
|
||||
private static func createAndSaveMosaic() {
|
||||
guard !DiskManager.mosaicExists else {
|
||||
event("Mosaic already created")
|
||||
return
|
||||
}
|
||||
updateMosaic()
|
||||
}
|
||||
|
||||
static var mosaic: UIImage? {
|
||||
guard let data = DiskManager.mosaicData else {
|
||||
error("No mosaic data on disk")
|
||||
return nil
|
||||
}
|
||||
guard let image = UIImage(data: data, scale: UIScreen.main.scale) else {
|
||||
error("Failed to create image from mosaic data")
|
||||
return nil
|
||||
}
|
||||
return image
|
||||
}
|
||||
|
||||
private static func updateMosaicWithNewCap(id: Int, _ image: UIImage) {
|
||||
guard let old = mosaic else { return }
|
||||
|
||||
let view = UIImageView(image: old)
|
||||
let canvas = makeMosaicCanvas()
|
||||
let tile = makeTile(id - 1, image: image)
|
||||
canvas.addSubview(view)
|
||||
canvas.addSubview(tile)
|
||||
guard let img = render(view: canvas) else {
|
||||
error("Failed to update mosaic for cap \(id)")
|
||||
return
|
||||
}
|
||||
saveMosaic(img)
|
||||
}
|
||||
|
||||
static func tile(for point: CGPoint) -> Int? {
|
||||
let s = point.y.truncatingRemainder(dividingBy: mosaicRowHeight)
|
||||
let row = Int(point.y / mosaicRowHeight)
|
||||
guard s > mosaicMargin else {
|
||||
return nil
|
||||
}
|
||||
let column: CGFloat
|
||||
if row.isEven {
|
||||
column = point.x / mosaicCellSize
|
||||
} else {
|
||||
column = (point.x - mosaicCellSize / 2) / mosaicCellSize
|
||||
}
|
||||
return row * mosaicColumns + Int(column)
|
||||
}
|
||||
|
||||
static func switchTilesInMosaic(_ mosaic: UIImageView, tile1: Int, tile2: Int) {
|
||||
let tileView1 = makeTile(tile1, image: Cap.tileImage(tile: tile1)!)
|
||||
let tileView2 = makeTile(tile2, image: Cap.tileImage(tile: tile2)!)
|
||||
|
||||
|
||||
mosaic.addSubview(tileView1)
|
||||
mosaic.addSubview(tileView2)
|
||||
defer {
|
||||
tileView1.removeFromSuperview()
|
||||
tileView2.removeFromSuperview()
|
||||
}
|
||||
|
||||
guard let img = render(view: mosaic) else {
|
||||
error("Failed to switch \(tile1) and \(tile2) in mosaic")
|
||||
return
|
||||
}
|
||||
|
||||
mosaic.image = img
|
||||
saveMosaic(img)
|
||||
}
|
||||
|
||||
static func updateMosaic() {
|
||||
guard let image = makeMosaic() else {
|
||||
error("No mosaik image created")
|
||||
return
|
||||
}
|
||||
saveMosaic(image)
|
||||
}
|
||||
|
||||
static func saveMosaic(_ image: UIImage) {
|
||||
guard let data = image.pngData() else {
|
||||
error("Failed to convert mosaic to data")
|
||||
return
|
||||
}
|
||||
guard DiskManager.saveMosaicData(data) else {
|
||||
error("Failed to write mosaic to disk")
|
||||
return
|
||||
}
|
||||
event("Mosaic saved")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Protocol Hashable
|
||||
|
||||
extension Cap: Hashable {
|
||||
|
||||
static func == (lhs: Cap, rhs: Cap) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Protocol CustomStringConvertible
|
||||
|
||||
extension Cap: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
return "\(id);\(name);\(count);\(tile)\n"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Protocol Logger
|
||||
|
||||
extension Cap: Logger {
|
||||
|
||||
static let logToken = "[CAP]"
|
||||
}
|
||||
|
||||
// MARK: - String extension
|
||||
|
||||
extension String {
|
||||
var clean: String {
|
||||
return lowercased().replacingOccurrences(of: "[^a-z0-9 ]", with: "", options: .regularExpression)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Int extension
|
||||
|
||||
private extension Int {
|
||||
|
||||
var isEven: Bool {
|
||||
return self % 2 == 0
|
||||
}
|
||||
}
|
214
CapCollector/Data/DiskManager.swift
Normal file
214
CapCollector/Data/DiskManager.swift
Normal file
@ -0,0 +1,214 @@
|
||||
//
|
||||
// DiskManager.swift
|
||||
// CapFinder
|
||||
//
|
||||
// Created by User on 23.04.18.
|
||||
// Copyright © 2018 User. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class DiskManager {
|
||||
|
||||
enum LocalDirectory: String {
|
||||
/// Folder for new images to upload
|
||||
case upload = "Upload"
|
||||
|
||||
/// Folder for downloaded images
|
||||
case images = "Images"
|
||||
|
||||
/// Folder for downloaded images
|
||||
case thumbnails = "Thumbnails"
|
||||
|
||||
/// Directory for name file
|
||||
case files = "Files"
|
||||
|
||||
private static let fm = FileManager.default
|
||||
|
||||
/// The url to the file sstem
|
||||
var url: URL {
|
||||
return URL(fileURLWithPath: self.rawValue, isDirectory: true, relativeTo: DiskManager.documentsDirectory)
|
||||
}
|
||||
|
||||
fileprivate func create() -> Bool {
|
||||
return DiskManager.create(directory: url)
|
||||
}
|
||||
|
||||
var content: [URL]? {
|
||||
do {
|
||||
return try LocalDirectory.fm.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])
|
||||
} catch {
|
||||
print("[LocalDirectory] Could not read directory \(self.rawValue): \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The folder where images and name list are stored
|
||||
static let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
|
||||
private static let fm = FileManager.default
|
||||
|
||||
// MARK: - First launch
|
||||
|
||||
@discardableResult static func setupOnFirstLaunch() -> Bool {
|
||||
return LocalDirectory.files.create() &&
|
||||
LocalDirectory.images.create() &&
|
||||
LocalDirectory.thumbnails.create() &&
|
||||
LocalDirectory.upload.create()
|
||||
}
|
||||
|
||||
private static func create(directory: URL) -> Bool {
|
||||
do {
|
||||
if !fm.fileExists(atPath: directory.path) {
|
||||
try fm.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
event("Could not create \(directory): \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image retrieval
|
||||
|
||||
/**
|
||||
Check if an image exists for a cap
|
||||
- parameter cap: The id of the cap
|
||||
- returns: True, if an image exists
|
||||
*/
|
||||
static func hasImage(for cap: Int) -> Bool {
|
||||
let url = localUrl(for: cap)
|
||||
return fm.fileExists(atPath: url.path)
|
||||
}
|
||||
|
||||
private static func localUrl(for cap: Int) -> URL {
|
||||
return URL(fileURLWithPath: "\(cap).jpg", isDirectory: true, relativeTo: LocalDirectory.images.url)
|
||||
}
|
||||
|
||||
private static func thumbnailUrl(for cap: Int) -> URL {
|
||||
return URL(fileURLWithPath: "\(cap).jpg", isDirectory: true, relativeTo: LocalDirectory.thumbnails.url)
|
||||
}
|
||||
|
||||
/**
|
||||
Get the image for a cap.
|
||||
If the image exists on disk, it is returned.
|
||||
If no image exists locally, then this function returns nil.
|
||||
- parameter cap: The id of the cap
|
||||
- returns: The image data, or `nil`
|
||||
*/
|
||||
static func image(for cap: Int) -> Data? {
|
||||
// If the image exists on disk, get it
|
||||
let url = localUrl(for: cap)
|
||||
return readData(from: url)
|
||||
}
|
||||
|
||||
private static let mosaicURL: URL = documentsDirectory.appendingPathComponent("mosaic.png")
|
||||
|
||||
|
||||
static var mosaicExists: Bool {
|
||||
return fm.fileExists(atPath: mosaicURL.path)
|
||||
}
|
||||
|
||||
static func saveMosaicData(_ data: Data) -> Bool {
|
||||
return write(data, to: mosaicURL)
|
||||
}
|
||||
|
||||
static var mosaicData: Data? {
|
||||
return readData(from: mosaicURL)
|
||||
}
|
||||
|
||||
/**
|
||||
Get the thumbnail for a cap.
|
||||
If the image exists on disk, it is returned.
|
||||
If no image exists locally, then this function returns nil.
|
||||
- parameter cap: The id of the cap
|
||||
- returns: The image data, or `nil`
|
||||
*/
|
||||
static func thumbnail(for cap: Int) -> Data? {
|
||||
// If the image exists on disk, get it
|
||||
let url = thumbnailUrl(for: cap)
|
||||
return readData(from: url)
|
||||
}
|
||||
|
||||
private static func readData(from url: URL) -> Data? {
|
||||
guard fm.fileExists(atPath: url.path) else {
|
||||
return nil
|
||||
}
|
||||
do {
|
||||
return try Data(contentsOf: url)
|
||||
} catch {
|
||||
self.error("Could not read data from \(url): \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Save an image to the download folder
|
||||
- parameter imageData: The data of the image
|
||||
- parameter cap: The cap id
|
||||
- returns: True, if the image was saved
|
||||
*/
|
||||
static func save(imageData: Data, for cap: Int) -> Bool {
|
||||
let url = localUrl(for: cap)
|
||||
return write(imageData, to: url)
|
||||
}
|
||||
|
||||
/**
|
||||
Save a thumbnail to the download folder
|
||||
- parameter thumbnailData: The data of the image
|
||||
- parameter cap: The cap id
|
||||
- returns: True, if the image was saved
|
||||
*/
|
||||
static func save(thumbnailData: Data, for cap: Int) -> Bool {
|
||||
let url = thumbnailUrl(for: cap)
|
||||
return write(thumbnailData, to: url)
|
||||
}
|
||||
|
||||
private static func write(_ data: Data, to url: URL) -> Bool {
|
||||
do {
|
||||
try data.write(to: url)
|
||||
} catch {
|
||||
self.error("Could not write data to \(url): \(error)")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static func removeFromUpload(url: URL) -> Bool {
|
||||
guard fm.fileExists(atPath: url.path) else {
|
||||
return true
|
||||
}
|
||||
do {
|
||||
try fm.removeItem(at: url)
|
||||
return true
|
||||
} catch {
|
||||
self.error("Could not delete file \(url.path)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static var pendingUploads: [URL]? {
|
||||
return LocalDirectory.upload.content
|
||||
}
|
||||
|
||||
static var availableImages: [Int]? {
|
||||
return LocalDirectory.images.content?.compactMap {
|
||||
Int($0.lastPathComponent.components(separatedBy: ".").first ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Save an image to the uploads folder for later
|
||||
*/
|
||||
static func saveForUpload(imageData: Data, name: String) -> URL? {
|
||||
let url = LocalDirectory.upload.url.appendingPathComponent(name)
|
||||
return write(imageData, to: url) ? url : nil
|
||||
}
|
||||
}
|
||||
|
||||
extension DiskManager: Logger {
|
||||
|
||||
static let logToken = "[DiskManager]"
|
||||
}
|
145
CapCollector/Data/NameFile.swift
Normal file
145
CapCollector/Data/NameFile.swift
Normal file
@ -0,0 +1,145 @@
|
||||
//
|
||||
// NameFile.swift
|
||||
// CapFinder
|
||||
//
|
||||
// Created by User on 23.04.18.
|
||||
// Copyright © 2018 User. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftyDropbox
|
||||
|
||||
final class NameFile: Logger {
|
||||
|
||||
static let logToken = "[NameFile]"
|
||||
|
||||
/// The name of the file
|
||||
private static let fileName = "names.txt"
|
||||
|
||||
private static let path = "/" + fileName
|
||||
|
||||
/// The url of the file on disk
|
||||
private static let url = DiskManager.documentsDirectory.appendingPathComponent(fileName)
|
||||
|
||||
private static let fm = FileManager.default
|
||||
|
||||
// MARK: - Reading from disk
|
||||
|
||||
/// Indicates if the name list was written to disk
|
||||
private static var nameFileExistsOnDisk: Bool {
|
||||
return fm.fileExists(atPath: url.path)
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
@discardableResult static func save(names: String) -> Bool {
|
||||
let data = names.data(using: .utf8)!
|
||||
return save(names: data)
|
||||
}
|
||||
|
||||
static func saveAndUpload(names: String, completion: @escaping (Bool) -> Void) {
|
||||
let data = names.data(using: .utf8)!
|
||||
guard save(names: data) else {
|
||||
return
|
||||
}
|
||||
|
||||
let client = DropboxController.client
|
||||
client.files.upload(path: path, mode: .overwrite, input: data).response { _ , error in
|
||||
if let error = error {
|
||||
self.error("Error uploading name list: \(error)")
|
||||
completion(false)
|
||||
} else {
|
||||
self.event("Uploaded name list")
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// The content of the name file as a String
|
||||
private static var content: String? {
|
||||
do {
|
||||
return try String(contentsOf: url, encoding: .utf8)
|
||||
} catch {
|
||||
self.error("Error reading \(url): \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Save the name file to disk
|
||||
- parameter names: The new name file content
|
||||
- returns: True, if the data was written to disk
|
||||
*/
|
||||
@discardableResult private static func save(names: Data) -> Bool {
|
||||
do {
|
||||
try names.write(to: url, options: .atomic)
|
||||
event("Name file saved to disk")
|
||||
return true
|
||||
} catch {
|
||||
self.error("Could not save names to file: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static func makeAvailable(completion: ((String?) -> Void)? = nil) {
|
||||
if nameFileExistsOnDisk {
|
||||
completion?(self.content)
|
||||
} else {
|
||||
download() { success in
|
||||
guard success else {
|
||||
completion?(nil)
|
||||
return
|
||||
}
|
||||
completion?(self.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The data of the name list
|
||||
private static var data: Data? {
|
||||
guard nameFileExistsOnDisk else { return nil }
|
||||
do {
|
||||
return try Data(contentsOf: url)
|
||||
} catch {
|
||||
self.error("Could not read data from \(url): \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Delete the file on disk
|
||||
- returns: True, if the file no longer exists on disk
|
||||
*/
|
||||
@discardableResult private static func delete() -> Bool {
|
||||
guard nameFileExistsOnDisk else {
|
||||
event("No name file to delete")
|
||||
return true
|
||||
}
|
||||
|
||||
do {
|
||||
try fm.removeItem(at: url)
|
||||
} catch {
|
||||
self.error("Could not delete name file: \(error)")
|
||||
return false
|
||||
}
|
||||
event("Deleted name file on disk")
|
||||
return true
|
||||
}
|
||||
|
||||
private static func download(completion: ((Bool) -> Void)? = nil) {
|
||||
let client = DropboxController.client
|
||||
event("Downloading names from Dropbox")
|
||||
client.files.download(path: path).response { response, error in
|
||||
guard let data = response?.1 else {
|
||||
self.error("Error downloading file: \(error!)")
|
||||
completion?(false)
|
||||
return
|
||||
}
|
||||
self.event("Downloaded name file")
|
||||
completion?(NameFile.save(names: data))
|
||||
}
|
||||
}
|
||||
}
|
@ -28,21 +28,51 @@ final class Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
static var notUploadedCapCount: Int {
|
||||
static var lastUploadedCapCount: Int {
|
||||
get {
|
||||
return UserDefaults.standard.integer(forKey: "notUploadedCaps")
|
||||
return UserDefaults.standard.integer(forKey: "lastUploadedCaps")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "notUploadedCaps")
|
||||
UserDefaults.standard.set(newValue, forKey: "lastUploadedCaps")
|
||||
}
|
||||
}
|
||||
|
||||
static var notUploadedImageCount: Int {
|
||||
static var lastUploadedImageCount: Int {
|
||||
get {
|
||||
return UserDefaults.standard.integer(forKey: "notUploadedImages")
|
||||
return UserDefaults.standard.integer(forKey: "lastUploadedImages")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "notUploadedImages")
|
||||
UserDefaults.standard.set(newValue, forKey: "lastUploadedImages")
|
||||
}
|
||||
}
|
||||
|
||||
static var squeezenet: Bool {
|
||||
get {
|
||||
return UserDefaults.standard.bool(forKey: "squeezenet")
|
||||
}
|
||||
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "squeezenet")
|
||||
}
|
||||
}
|
||||
|
||||
static var resnet: Bool {
|
||||
get {
|
||||
return UserDefaults.standard.bool(forKey: "resnet")
|
||||
}
|
||||
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "resnet")
|
||||
}
|
||||
}
|
||||
|
||||
static var xcode: Bool {
|
||||
get {
|
||||
return UserDefaults.standard.bool(forKey: "xcode")
|
||||
}
|
||||
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "xcode")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.2.6</string>
|
||||
<string>1.3</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
@ -40,6 +40,24 @@
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Take images to identify matching caps and register new ones</string>
|
||||
<key>UIApplicationShortcutItems</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UIApplicationShortcutItemIconType</key>
|
||||
<string>UIApplicationShortcutIconTypeCapturePhoto</string>
|
||||
<key>UIApplicationShortcutItemSubtitle</key>
|
||||
<string>Compare a new image</string>
|
||||
<key>UIApplicationShortcutItemTitle</key>
|
||||
<string>Take image</string>
|
||||
<key>UIApplicationShortcutItemType</key>
|
||||
<string>firstShortcut</string>
|
||||
<key>UIApplicationShortcutItemUserInfo</key>
|
||||
<dict>
|
||||
<key>firstShortcutKey1</key>
|
||||
<string>firstShortcutKeyValue1</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
@ -53,6 +71,8 @@
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
|
35
CapCollector/Logger.swift
Normal file
35
CapCollector/Logger.swift
Normal file
@ -0,0 +1,35 @@
|
||||
//
|
||||
// Logger.swift
|
||||
// CapFinder
|
||||
//
|
||||
// Created by User on 11.04.18.
|
||||
// Copyright © 2018 User. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol Logger {
|
||||
|
||||
static var logToken: String { get }
|
||||
|
||||
}
|
||||
|
||||
extension Logger {
|
||||
|
||||
func error(_ message: String) {
|
||||
print(Self.logToken + " ERROR: " + message)
|
||||
}
|
||||
|
||||
func event(_ message: String) {
|
||||
print(Self.logToken + " " + message)
|
||||
}
|
||||
|
||||
static func error(_ message: String) {
|
||||
print(logToken + " ERROR: " + message)
|
||||
}
|
||||
|
||||
static func event(_ message: String) {
|
||||
print(logToken + " " + message)
|
||||
}
|
||||
|
||||
}
|
Binary file not shown.
46
CapCollector/Presentation/CapCell.swift
Normal file
46
CapCollector/Presentation/CapCell.swift
Normal file
@ -0,0 +1,46 @@
|
||||
//
|
||||
// CapCell.swift
|
||||
// CapFinder
|
||||
//
|
||||
// Created by User on 22.04.18.
|
||||
// Copyright © 2018 User. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
|
||||
class CapCell: UITableViewCell {
|
||||
|
||||
@IBOutlet private weak var capImage: RoundedImageView!
|
||||
|
||||
@IBOutlet private weak var matchLabel: UILabel!
|
||||
|
||||
@IBOutlet private weak var nameLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var countLabel: UILabel!
|
||||
|
||||
var id = 0
|
||||
|
||||
var cap: Cap! {
|
||||
didSet {
|
||||
updateCell()
|
||||
}
|
||||
}
|
||||
|
||||
func updateCell() {
|
||||
capImage.image = cap.image
|
||||
//capImage.borderColor = AppDelegate.tintColor
|
||||
|
||||
matchLabel.text = text(for: cap.match)
|
||||
nameLabel.text = cap.name
|
||||
countLabel.text = "\(cap.id) (\(cap.count) image" + (cap.count > 1 ? "s)" : ")")
|
||||
}
|
||||
|
||||
private func text(for value: Float?) -> String? {
|
||||
guard let nr = value else {
|
||||
return nil
|
||||
}
|
||||
let percent = Int((nr * 100).rounded())
|
||||
return String(format: "%d %%", arguments: [percent])
|
||||
}
|
||||
}
|
273
CapCollector/Presentation/GridViewController.swift
Normal file
273
CapCollector/Presentation/GridViewController.swift
Normal file
@ -0,0 +1,273 @@
|
||||
//
|
||||
// GridViewController.swift
|
||||
// CapCollector
|
||||
//
|
||||
// Created by Christoph on 07.01.19.
|
||||
// Copyright © 2019 CH. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class GridViewController: UIViewController {
|
||||
|
||||
|
||||
|
||||
private let columns = 40
|
||||
|
||||
static let len: CGFloat = 60
|
||||
|
||||
private lazy var rowHeight = GridViewController.len * 0.866
|
||||
|
||||
private lazy var margin = GridViewController.len - rowHeight
|
||||
|
||||
private var myView: UIView!
|
||||
|
||||
private var canvasSize: CGSize = .zero
|
||||
|
||||
@IBOutlet weak var scrollView: UIScrollView!
|
||||
|
||||
private var selectedTile: Int? = nil
|
||||
|
||||
private weak var selectionView: RoundedButton!
|
||||
|
||||
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
override var shouldAutorotate: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let width = CGFloat(columns) * GridViewController.len + GridViewController.len / 2
|
||||
let height = (CGFloat(Cap.totalCapCount) / CGFloat(columns)).rounded(.up) * rowHeight + margin
|
||||
canvasSize = CGSize(width: width, height: height)
|
||||
myView = UIView(frame: CGRect(origin: .zero, size: canvasSize))
|
||||
|
||||
|
||||
scrollView.addSubview(myView)
|
||||
scrollView.contentSize = canvasSize
|
||||
scrollView.delegate = self
|
||||
scrollView.zoomScale = 0.5
|
||||
scrollView.maximumZoomScale = 1
|
||||
setZoomRange()
|
||||
|
||||
/*
|
||||
guard let image = Cap.mosaic else {
|
||||
error("No mosaic")
|
||||
return
|
||||
}
|
||||
imageView.image = image
|
||||
imageHeight.constant = image.size.height
|
||||
imageWidth.constant = image.size.width
|
||||
scrollView.contentSize = image.size
|
||||
let button = RoundedButton(frame: CGRect(origin: .zero, size: CGSize(width: Cap.mosaicCellSize, height: Cap.mosaicCellSize)))
|
||||
imageView.addSubview(button)
|
||||
selectionView = button
|
||||
button.borderColor = AppDelegate.tintColor
|
||||
button.borderWidth = 3
|
||||
button.isHidden = true
|
||||
scrollView.delegate = self
|
||||
scrollView.zoomScale = 1
|
||||
scrollView.maximumZoomScale = 1
|
||||
setZoomRange()
|
||||
|
||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
||||
scrollView.addGestureRecognizer(tapRecognizer)
|
||||
event("did load")
|
||||
*/
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
updateTiles()
|
||||
|
||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
||||
myView.addGestureRecognizer(tapRecognizer)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
setZoomRange()
|
||||
updateTiles()
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
Cap.save()
|
||||
}
|
||||
|
||||
private func setZoomRange() {
|
||||
let size = scrollView.frame.size
|
||||
let a = size.width / canvasSize.width
|
||||
let b = size.height / canvasSize.height
|
||||
let scale = min(a,b)
|
||||
scrollView.minimumZoomScale = min(a,b)
|
||||
if scrollView.zoomScale < scale {
|
||||
scrollView.setZoomScale(scale, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleTap(_ sender: UITapGestureRecognizer) {
|
||||
|
||||
let loc = sender.location(in: myView)
|
||||
let y = loc.y
|
||||
let s = y.truncatingRemainder(dividingBy: rowHeight)
|
||||
let row = Int(y / rowHeight)
|
||||
guard s > margin else {
|
||||
return
|
||||
}
|
||||
let column: CGFloat
|
||||
if row.isEven {
|
||||
column = loc.x / GridViewController.len
|
||||
} else {
|
||||
column = (loc.x - GridViewController.len / 2) / GridViewController.len
|
||||
}
|
||||
handleTileTapped(tile: row * columns + Int(column))
|
||||
|
||||
/*
|
||||
event("Tapped")
|
||||
let loc = sender.location(in: imageView)
|
||||
guard let tile = Cap.tile(for: loc) else {
|
||||
event("No tile for location \(loc)")
|
||||
return
|
||||
}
|
||||
handleTileTapped(tile: tile)
|
||||
*/
|
||||
}
|
||||
|
||||
private func handleTileTapped(tile: Int) {
|
||||
if let selected = selectedTile {
|
||||
switchTiles(oldTile: selected, newTile: tile)
|
||||
} else {
|
||||
showSelection(tile: tile)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
private func showSelection(tile: Int) {
|
||||
event("Selecting tile \(tile)")
|
||||
let point = Cap.origin(for: tile)
|
||||
selectionView.frame = CGRect(origin: point, size: selectionView.frame.size)
|
||||
selectionView.isHidden = false
|
||||
selectedTile = tile
|
||||
}
|
||||
|
||||
private func switchTiles(oldTile: Int, newTile: Int) {
|
||||
event("Switching tiles \(oldTile) and \(newTile)")
|
||||
selectionView.isHidden = true
|
||||
selectedTile = nil
|
||||
guard oldTile != newTile else {
|
||||
return
|
||||
}
|
||||
Cap.switchTiles(oldTile, newTile)
|
||||
Cap.switchTilesInMosaic(imageView, tile1: oldTile, tile2: newTile)
|
||||
}
|
||||
*/
|
||||
|
||||
private var installedTiles = [Int : RoundedImageView]()
|
||||
|
||||
private func showSelection(tile: Int) {
|
||||
clearTileSelection()
|
||||
|
||||
if let view = installedTiles[tile] {
|
||||
view.borderWidth = 3
|
||||
view.borderColor = AppDelegate.tintColor
|
||||
selectedTile = tile
|
||||
} else {
|
||||
selectedTile = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func tileIsVisible(tile: Int, in rect: CGRect) -> Bool {
|
||||
return rect.intersects(frame(for: tile))
|
||||
}
|
||||
|
||||
private func makeTile(_ tile: Int) {
|
||||
let view = RoundedImageView(frame: frame(for: tile))
|
||||
myView.addSubview(view)
|
||||
view.image = Cap.tileImage(tile: tile)
|
||||
installedTiles[tile] = view
|
||||
}
|
||||
|
||||
private func frame(for tile: Int) -> CGRect {
|
||||
let row = tile / columns
|
||||
let column = tile - row * columns
|
||||
let x = CGFloat(column) * GridViewController.len + (row.isEven ? 0 : GridViewController.len / 2)
|
||||
let y = CGFloat(row) * rowHeight
|
||||
return CGRect(x: x, y: y, width: GridViewController.len, height: GridViewController.len)
|
||||
}
|
||||
|
||||
private func switchTiles(oldTile: Int, newTile: Int) {
|
||||
if oldTile != newTile {
|
||||
Cap.switchTiles(oldTile, newTile)
|
||||
installedTiles[oldTile]?.image = Cap.tileImage(tile: oldTile)
|
||||
installedTiles[newTile]?.image = Cap.tileImage(tile: newTile)
|
||||
}
|
||||
clearTileSelection()
|
||||
}
|
||||
|
||||
private func clearTileSelection() {
|
||||
guard let tile = selectedTile else {
|
||||
return
|
||||
}
|
||||
installedTiles[tile]?.borderWidth = 0
|
||||
selectedTile = nil
|
||||
}
|
||||
|
||||
private func showTiles(in rect: CGRect) {
|
||||
for i in 0..<Cap.totalCapCount {
|
||||
if tileIsVisible(tile: i, in: rect) {
|
||||
if installedTiles[i] != nil {
|
||||
continue
|
||||
}
|
||||
makeTile(i)
|
||||
} else if let tile = installedTiles[i] {
|
||||
tile.removeFromSuperview()
|
||||
installedTiles[i] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateTiles() {
|
||||
guard #available(iOS 12.0, *) else {
|
||||
return
|
||||
}
|
||||
let scale = scrollView.zoomScale
|
||||
let offset = scrollView.contentOffset
|
||||
let size = scrollView.visibleSize
|
||||
|
||||
let scaledOrigin = CGPoint(x: offset.x / scale, y: offset.y / scale)
|
||||
let scaledSize = CGSize(width: size.width / scale, height: size.height / scale)
|
||||
let rect = CGRect(origin: scaledOrigin, size: scaledSize)
|
||||
|
||||
showTiles(in: rect)
|
||||
}
|
||||
}
|
||||
|
||||
extension GridViewController: UIScrollViewDelegate {
|
||||
|
||||
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||
return myView
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
updateTiles()
|
||||
}
|
||||
}
|
||||
|
||||
private extension Int {
|
||||
|
||||
var isEven: Bool {
|
||||
return self % 2 == 0
|
||||
}
|
||||
}
|
||||
|
||||
extension GridViewController: Logger {
|
||||
|
||||
static let logToken: String = "[Grid]"
|
||||
|
||||
}
|
15
CapCollector/Presentation/ImageCell.swift
Normal file
15
CapCollector/Presentation/ImageCell.swift
Normal file
@ -0,0 +1,15 @@
|
||||
//
|
||||
// ImageCell.swift
|
||||
// CapFinder
|
||||
//
|
||||
// Created by User on 07.02.18.
|
||||
// Copyright © 2018 User. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ImageCell: UICollectionViewCell {
|
||||
|
||||
@IBOutlet weak var capView: UIImageView!
|
||||
|
||||
}
|
166
CapCollector/Presentation/ImageSelector.swift
Normal file
166
CapCollector/Presentation/ImageSelector.swift
Normal file
@ -0,0 +1,166 @@
|
||||
//
|
||||
// ListViewController.swift
|
||||
// CapFinder
|
||||
//
|
||||
// Created by User on 22.02.18.
|
||||
// Copyright © 2018 User. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftyDropbox
|
||||
|
||||
class ImageSelector: UIViewController {
|
||||
|
||||
// MARK: - Constants
|
||||
|
||||
/// The number of items per row
|
||||
private let itemsPerRow: CGFloat = 3
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
override var shouldAutorotate: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - CollectionView
|
||||
|
||||
private var images = [UIImage?]()
|
||||
|
||||
var cap: Cap!
|
||||
|
||||
@IBOutlet weak var collection: UICollectionView!
|
||||
|
||||
// MARK: - Life cycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
collection.dataSource = self
|
||||
collection.delegate = self
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
downloadImages()
|
||||
}
|
||||
|
||||
// MARK: Image download
|
||||
|
||||
private func downloadImages() {
|
||||
images = [UIImage?](repeating: nil, count: cap.count)
|
||||
event("\(cap.count) images for cap \(cap.id)")
|
||||
for number in 0..<cap.count {
|
||||
cap.downloadImage(number) { image in
|
||||
self.images[number] = image
|
||||
self.collection.reloadItems(at: [IndexPath(row: number, section: 0)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Select
|
||||
|
||||
private func selectedImage(nr: Int) {
|
||||
guard images[nr] != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
let tempId = cap.count
|
||||
let tempFile = "/Images/\(cap.id)/\(cap.id)-\(tempId).jpg"
|
||||
let oldFile = "/Images/\(cap.id)/\(cap.id)-0.jpg"
|
||||
let newFile = "/Images/\(cap.id)/\(cap.id)-\(nr).jpg"
|
||||
guard oldFile != newFile else {
|
||||
return
|
||||
}
|
||||
DropboxController.shared.move(file: oldFile, to: tempFile) { success in
|
||||
guard success else {
|
||||
self.error("Could not move \(oldFile) to \(tempFile)")
|
||||
return
|
||||
}
|
||||
DropboxController.shared.move(file: newFile, to: oldFile) { success in
|
||||
guard success else {
|
||||
self.error("Could not move \(newFile) to \(oldFile)")
|
||||
return
|
||||
}
|
||||
DropboxController.shared.move(file: tempFile, to: newFile) { success in
|
||||
if !success {
|
||||
self.error("Could not move \(tempFile) to \(newFile)")
|
||||
}
|
||||
self.finish(with: nr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func finish(with nr: Int) {
|
||||
let image = images[nr]!
|
||||
guard cap.save(mainImage: image) else {
|
||||
return
|
||||
}
|
||||
event("Successfully switched image")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDataSource
|
||||
|
||||
extension ImageSelector: UICollectionViewDataSource {
|
||||
|
||||
func numberOfSections(in collectionView: UICollectionView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return images.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||||
let cell = collectionView.dequeueReusableCell(
|
||||
withReuseIdentifier: "Image",
|
||||
for: indexPath) as! ImageCell
|
||||
|
||||
cell.capView.image = images[indexPath.row]
|
||||
return cell
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||
selectedImage(nr: indexPath.row)
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UICollectionViewDelegateFlowLayout
|
||||
|
||||
extension ImageSelector : UICollectionViewDelegateFlowLayout {
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
let widthPerItem = collectionView.frame.width / itemsPerRow
|
||||
return CGSize(width: widthPerItem, height: widthPerItem)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
insetForSectionAt section: Int) -> UIEdgeInsets {
|
||||
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView,
|
||||
layout collectionViewLayout: UICollectionViewLayout,
|
||||
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageSelector: Logger {
|
||||
|
||||
static let logToken = "[ImageSelector]"
|
||||
}
|
||||
|
||||
|
33
CapCollector/Presentation/NavigationController.swift
Normal file
33
CapCollector/Presentation/NavigationController.swift
Normal file
@ -0,0 +1,33 @@
|
||||
//
|
||||
// NavigationController.swift
|
||||
// CapCollector
|
||||
//
|
||||
// Created by Christoph on 08.01.19.
|
||||
// Copyright © 2019 CH. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class NavigationController: UINavigationController {
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return allowLandscape ? .allButUpsideDown : .portrait
|
||||
}
|
||||
|
||||
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
override var shouldAutorotate: Bool {
|
||||
return allowLandscape
|
||||
}
|
||||
|
||||
var allowLandscape: Bool = false
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
// Do any additional setup after loading the view.
|
||||
}
|
||||
|
||||
}
|
239
CapCollector/Presentation/SettingsController.swift
Normal file
239
CapCollector/Presentation/SettingsController.swift
Normal file
@ -0,0 +1,239 @@
|
||||
//
|
||||
// SettingsController.swift
|
||||
// CapCollector
|
||||
//
|
||||
// Created by Christoph on 15.10.18.
|
||||
// Copyright © 2018 CH. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SettingsController: UITableViewController {
|
||||
|
||||
@IBOutlet weak var totalCapsLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var totalImagesLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var recognizedCapsLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var imagesStatsLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var databaseUpdatesLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var dropboxAccountLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var countsLabel: UILabel!
|
||||
|
||||
private var nameFileChanges = false
|
||||
|
||||
private var isUpdatingCounts = false
|
||||
|
||||
private var isUpdatingThumbnails = false
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .portrait
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
updateDropboxStatus()
|
||||
updateNameFileStats()
|
||||
updateDatabaseStats()
|
||||
|
||||
(navigationController as! NavigationController).allowLandscape = false
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
setAccessories()
|
||||
}
|
||||
|
||||
private func updateThumbnails() {
|
||||
isUpdatingThumbnails = true
|
||||
for cap in Cap.all.values {
|
||||
cap.makeThumbnail()
|
||||
}
|
||||
isUpdatingThumbnails = false
|
||||
}
|
||||
|
||||
private func updateDatabaseStats() {
|
||||
let totalCaps = Cap.totalCapCount
|
||||
totalCapsLabel.text = "\(totalCaps) caps"
|
||||
totalImagesLabel.text = "\(Cap.imageCount) images"
|
||||
let recognizedCaps = Persistence.recognizedCapCount
|
||||
let newCapCount = totalCaps - recognizedCaps
|
||||
recognizedCapsLabel.text = "\(newCapCount) new caps"
|
||||
let ratio = Float(Cap.imageCount)/Float(Cap.totalCapCount)
|
||||
let (images, count) = Cap.getCapStatistics().enumerated().first(where: { $0.element != 0 })!
|
||||
imagesStatsLabel.text = String(format: "%.2f images per cap, lowest count: %d (%dx)", ratio, images, count)
|
||||
}
|
||||
|
||||
private func updateNameFileStats() {
|
||||
let capCount = Cap.totalCapCount - Persistence.lastUploadedCapCount
|
||||
let imageCount = Cap.imageCount - Persistence.lastUploadedImageCount
|
||||
nameFileChanges = capCount > 0 || imageCount > 0
|
||||
databaseUpdatesLabel.text = "\(capCount) new caps and \(imageCount) new images"
|
||||
}
|
||||
|
||||
private func updateDropboxStatus() {
|
||||
dropboxAccountLabel.text = DropboxController.shared.isEnabled ? "Sign out" : "Sign in"
|
||||
}
|
||||
|
||||
private func setAccessories() {
|
||||
tableView.cellForRow(at: IndexPath(row: 0, section: 2))?.accessoryType = Persistence.squeezenet ? .checkmark : .none
|
||||
tableView.cellForRow(at: IndexPath(row: 1, section: 2))?.accessoryType = Persistence.resnet ? .checkmark : .none
|
||||
tableView.cellForRow(at: IndexPath(row: 2, section: 2))?.accessoryType = Persistence.xcode ? .checkmark : .none
|
||||
}
|
||||
|
||||
private func toggleClassifier(index: Int) {
|
||||
switch index {
|
||||
case 0: Persistence.squeezenet = !Persistence.squeezenet
|
||||
case 1: Persistence.resnet = !Persistence.resnet
|
||||
case 2: Persistence.xcode = !Persistence.xcode
|
||||
default:
|
||||
return
|
||||
}
|
||||
setAccessories()
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
||||
switch indexPath.section {
|
||||
case 0: // Mosaic
|
||||
return true
|
||||
case 1: // Database
|
||||
return indexPath.row == 2 && nameFileChanges
|
||||
case 2: // Choose models
|
||||
return true
|
||||
case 3: // Refresh
|
||||
if indexPath.row == 0 {
|
||||
return !isUpdatingCounts
|
||||
} else {
|
||||
return !isUpdatingThumbnails
|
||||
}
|
||||
case 4: // Dropbox account
|
||||
return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
switch indexPath.section {
|
||||
case 0: // Mosaic
|
||||
return indexPath
|
||||
case 1: // Database
|
||||
return (indexPath.row == 2 && nameFileChanges) ? indexPath : nil
|
||||
case 2: // Choose models
|
||||
return indexPath
|
||||
case 3: // Refresh count
|
||||
if indexPath.row == 0 {
|
||||
return isUpdatingCounts ? nil : indexPath
|
||||
} else {
|
||||
return isUpdatingThumbnails ? nil : indexPath
|
||||
}
|
||||
case 4: // Dropbox account
|
||||
return indexPath
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
switch indexPath.section {
|
||||
case 1: // Upload
|
||||
if indexPath.row == 2 && nameFileChanges {
|
||||
uploadNameFile()
|
||||
}
|
||||
case 2: // Choose models
|
||||
toggleClassifier(index: indexPath.row)
|
||||
case 3: // Refresh count
|
||||
if indexPath.row == 0 {
|
||||
updateCounts()
|
||||
} else {
|
||||
updateThumbnails()
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCounts() {
|
||||
isUpdatingCounts = true
|
||||
Cap.shouldSave = false
|
||||
// TODO: Don't make all requests at the same time
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let list = Cap.all.keys.sorted()
|
||||
let total = list.count
|
||||
var finished = 0
|
||||
let chunks = list.chunked(into: 10)
|
||||
for chunk in chunks {
|
||||
self.updateCounts(ids: chunk)
|
||||
finished += 10
|
||||
DispatchQueue.main.async {
|
||||
self.countsLabel.text = "\(finished) / \(total) finished"
|
||||
}
|
||||
}
|
||||
self.isUpdatingCounts = false
|
||||
Cap.shouldSave = true
|
||||
DispatchQueue.main.async {
|
||||
self.countsLabel.text = "Refresh image counts"
|
||||
self.updateDatabaseStats()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCounts(ids: [Int]) {
|
||||
var count = ids.count
|
||||
let s = DispatchSemaphore(value: 0)
|
||||
for cap in ids {
|
||||
Cap.all[cap]!.updateCount { _ in
|
||||
count -= 1
|
||||
if count == 0 {
|
||||
s.signal()
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = s.wait(timeout: .now() + .seconds(30))
|
||||
event("Finished updating ids \(ids.first!) to \(ids.last!)")
|
||||
}
|
||||
|
||||
private func uploadNameFile() {
|
||||
Cap.saveAndUpload() { _ in
|
||||
self.updateNameFileStats()
|
||||
}
|
||||
}
|
||||
|
||||
private func toggleDropbox() {
|
||||
guard !DropboxController.shared.isEnabled else {
|
||||
DropboxController.shared.signOut()
|
||||
updateDropboxStatus()
|
||||
return
|
||||
}
|
||||
|
||||
DropboxController.shared.setup(in: self)
|
||||
updateDropboxStatus()
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
guard let id = segue.identifier, id == "showMosaic" else {
|
||||
return
|
||||
}
|
||||
(navigationController as! NavigationController).allowLandscape = true
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsController: Logger {
|
||||
|
||||
static let logToken = "[Settings]"
|
||||
}
|
||||
|
||||
private extension Array {
|
||||
func chunked(into size: Int) -> [[Element]] {
|
||||
return stride(from: 0, to: count, by: size).map {
|
||||
Array(self[$0 ..< Swift.min($0 + size, count)])
|
||||
}
|
||||
}
|
||||
}
|
92
CapCollector/Presentation/SortController.swift
Normal file
92
CapCollector/Presentation/SortController.swift
Normal file
@ -0,0 +1,92 @@
|
||||
//
|
||||
// SortController.swift
|
||||
// CapCollector
|
||||
//
|
||||
// Created by Christoph on 12.11.18.
|
||||
// Copyright © 2018 CH. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
enum SortCriteria: Int {
|
||||
case id = 0
|
||||
case name = 1
|
||||
case count = 2
|
||||
case match = 3
|
||||
}
|
||||
|
||||
protocol SortControllerDelegate {
|
||||
|
||||
func sortController(didSelect sortType: SortCriteria, ascending: Bool)
|
||||
}
|
||||
|
||||
class SortController: UITableViewController {
|
||||
|
||||
var selected: SortCriteria = .count
|
||||
|
||||
var ascending: Bool = true
|
||||
|
||||
var delegate: SortControllerDelegate?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let height = Cap.hasMatches ? 310 : 270
|
||||
preferredContentSize = CGSize(width: 200, height: height)
|
||||
}
|
||||
|
||||
private func setCell() {
|
||||
for i in 0..<4 {
|
||||
let index = IndexPath(row: i, section: 1)
|
||||
let cell = tableView.cellForRow(at: index)
|
||||
cell?.accessoryType = i == selected.rawValue ? .checkmark : .none
|
||||
}
|
||||
let index = IndexPath(row: 0, section: 0)
|
||||
let cell = tableView.cellForRow(at: index)
|
||||
cell?.accessoryType = ascending ? .checkmark : .none
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
setCell()
|
||||
}
|
||||
|
||||
private func giveFeedback(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
|
||||
let generator = UIImpactFeedbackGenerator(style: style)
|
||||
generator.impactOccurred()
|
||||
}
|
||||
|
||||
// MARK: - Table view data source
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
guard indexPath.section == 1 else {
|
||||
ascending = !ascending
|
||||
setCell()
|
||||
delegate?.sortController(didSelect: selected, ascending: ascending)
|
||||
giveFeedback(.light)
|
||||
return
|
||||
}
|
||||
giveFeedback(.medium)
|
||||
selected = SortCriteria(rawValue: indexPath.row)!
|
||||
delegate?.sortController(didSelect: selected, ascending: ascending)
|
||||
self.dismiss(animated: true)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
guard indexPath.row == 3 else { return indexPath }
|
||||
return Cap.hasMatches ? indexPath : nil
|
||||
}
|
||||
|
||||
/*
|
||||
// MARK: - Navigation
|
||||
|
||||
// In a storyboard-based application, you will often want to do a little preparation before navigation
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
// Get the new view controller using segue.destination.
|
||||
// Pass the selected object to the new view controller.
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
@ -1,144 +0,0 @@
|
||||
//
|
||||
// SettingsController.swift
|
||||
// CapCollector
|
||||
//
|
||||
// Created by Christoph on 15.10.18.
|
||||
// Copyright © 2018 CH. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SettingsController: UITableViewController {
|
||||
|
||||
@IBOutlet weak var totalCapsLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var recognizedCapsLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var databaseUpdatesLabel: UILabel!
|
||||
|
||||
@IBOutlet weak var dropboxAccountLabel: UILabel!
|
||||
|
||||
private var nameFileChanges = false
|
||||
|
||||
private var isUpdatingCounts = false
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
updateDropboxStatus()
|
||||
updateNameFileStats()
|
||||
updateDatabaseStats()
|
||||
}
|
||||
|
||||
private func updateDatabaseStats() {
|
||||
let totalCaps = CapNames.shared.count
|
||||
totalCapsLabel.text = "\(totalCaps) caps, \(CapNames.shared.imageCount) images"
|
||||
|
||||
let recognizedCaps = Persistence.recognizedCapCount
|
||||
let newCapCount = totalCaps - recognizedCaps
|
||||
recognizedCapsLabel.text = "\(recognizedCaps) recognized, \(newCapCount) new"
|
||||
}
|
||||
|
||||
private func updateNameFileStats() {
|
||||
let capCount = Persistence.notUploadedCapCount
|
||||
let imageCount = Persistence.notUploadedImageCount
|
||||
nameFileChanges = capCount > 0 || imageCount > 0
|
||||
databaseUpdatesLabel.text = "\(capCount) new caps and \(imageCount) new images"
|
||||
}
|
||||
|
||||
private func updateDropboxStatus() {
|
||||
print("Dropbox enabled: \(DropboxController.shared.isEnabled)")
|
||||
dropboxAccountLabel.text = DropboxController.shared.isEnabled ? "Sign out" : "Sign in"
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
||||
switch indexPath.section {
|
||||
case 0: // Database
|
||||
return false
|
||||
case 1: // Upload
|
||||
return nameFileChanges
|
||||
case 2: // Refresh count
|
||||
return !isUpdatingCounts
|
||||
case 3: // Dropbox account
|
||||
return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
switch indexPath.section {
|
||||
case 0: // Database
|
||||
return nil
|
||||
case 1: // Upload
|
||||
return nameFileChanges ? indexPath : nil
|
||||
case 2: // Refresh count
|
||||
return isUpdatingCounts ? nil : indexPath
|
||||
case 3: // Dropbox account
|
||||
return indexPath
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
switch indexPath.section {
|
||||
case 0:
|
||||
break
|
||||
case 1: // Upload
|
||||
if nameFileChanges {
|
||||
uploadNameFile()
|
||||
}
|
||||
case 2: // Refresh count
|
||||
updateCounts()
|
||||
case 3: // Dropbox account
|
||||
break
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCounts() {
|
||||
isUpdatingCounts = true
|
||||
CapImages.shared.updateCounts() {
|
||||
self.isUpdatingCounts = false
|
||||
self.updateDatabaseStats()
|
||||
}
|
||||
}
|
||||
|
||||
private func uploadNameFile() {
|
||||
CapNames.shared.saveAndUpload()
|
||||
updateNameFileStats()
|
||||
}
|
||||
|
||||
private func toggleDropbox() {
|
||||
guard !DropboxController.shared.isEnabled else {
|
||||
DropboxController.shared.signOut()
|
||||
updateDropboxStatus()
|
||||
return
|
||||
}
|
||||
|
||||
DropboxController.shared.setup(in: self)
|
||||
updateDropboxStatus()
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsController: DropboxControllerDelegate {
|
||||
|
||||
func dropboxControllerDidFinishLoadingCaps() {
|
||||
|
||||
}
|
||||
|
||||
func dropboxControllerDidLoadNames() {
|
||||
|
||||
}
|
||||
|
||||
func dropboxController(didLoad caps: [Int]) {
|
||||
|
||||
}
|
||||
|
||||
func dropboxController(didLoad cap: Int) {
|
||||
|
||||
}
|
||||
}
|
89
CapCollector/Sync/DropBoxController.swift
Normal file
89
CapCollector/Sync/DropBoxController.swift
Normal file
@ -0,0 +1,89 @@
|
||||
//
|
||||
// DropboxController.swift
|
||||
// CapFinder
|
||||
//
|
||||
// Created by User on 08.04.18.
|
||||
// Copyright © 2018 User. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftyDropbox
|
||||
import UIKit
|
||||
|
||||
class DropboxController: Logger {
|
||||
|
||||
static var logToken = "[Dropbox]"
|
||||
|
||||
static var shared = DropboxController()
|
||||
|
||||
// MARK: Dropbox API
|
||||
|
||||
static var client: DropboxClient {
|
||||
return DropboxClientsManager.authorizedClient!
|
||||
}
|
||||
|
||||
var isEnabled: Bool {
|
||||
return DropboxClientsManager.authorizedClient != nil
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init() {
|
||||
|
||||
}
|
||||
|
||||
/** Register dropbox, load names and images.
|
||||
- parameter viewController: The controller launching the request
|
||||
*/
|
||||
func setup(in viewController: UIViewController) {
|
||||
guard isEnabled == false else {
|
||||
event("Enabled")
|
||||
return
|
||||
}
|
||||
event("Requesting access")
|
||||
DropboxClientsManager.authorizeFromController(
|
||||
UIApplication.shared,
|
||||
controller: viewController) {
|
||||
UIApplication.shared.open($0, options: [:])
|
||||
}
|
||||
}
|
||||
|
||||
func signOut() {
|
||||
DropboxClientsManager.unlinkClients()
|
||||
}
|
||||
|
||||
/// Process the response of the dropbox registration handler
|
||||
func handle(url: URL) {
|
||||
guard let authResult = DropboxClientsManager.handleRedirectURL(url) else {
|
||||
return
|
||||
}
|
||||
switch authResult {
|
||||
case .success:
|
||||
event("enabled")
|
||||
case .cancel:
|
||||
error("Authorization flow canceled")
|
||||
case .error(_, let description):
|
||||
error("Error on authentification: \(description)")
|
||||
}
|
||||
}
|
||||
|
||||
/// Download name list and cap images
|
||||
func initializeDatabase() {
|
||||
guard isEnabled else {
|
||||
event("Dropbox not enabled")
|
||||
return
|
||||
}
|
||||
Cap.load()
|
||||
}
|
||||
|
||||
func move(file: String, to: String, completion: @escaping (Bool) -> Void) {
|
||||
DropboxController.client.files.moveV2(fromPath: file, toPath: to).response { _, error in
|
||||
if let err = error {
|
||||
self.error("Failed to move file: \(err)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
}
|
528
CapCollector/TableView.swift
Normal file
528
CapCollector/TableView.swift
Normal file
@ -0,0 +1,528 @@
|
||||
//
|
||||
// TableView.swift
|
||||
// CapFinder
|
||||
//
|
||||
// Created by User on 22.04.18.
|
||||
// Copyright © 2018 User. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class TableView: UIViewController {
|
||||
|
||||
@IBOutlet weak var table: UITableView!
|
||||
|
||||
@IBOutlet weak var searchBar: UISearchBar!
|
||||
|
||||
@IBOutlet weak var searchBarConstraint: NSLayoutConstraint!
|
||||
|
||||
@IBOutlet weak var imageButtonConstraint: NSLayoutConstraint!
|
||||
|
||||
private let imageViewDistance: CGFloat = 90
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
// MARK: - Life cycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
Classifier.shared.delegate = self
|
||||
Cap.delegate = self
|
||||
tableViewSetup()
|
||||
imageButtonSetup()
|
||||
searchBar.delegate = self
|
||||
searchBarSetup()
|
||||
self.imageViewContraint.constant = imageViewDistance
|
||||
DropboxController.shared.initializeDatabase()
|
||||
}
|
||||
|
||||
|
||||
private var caps = [Cap]()
|
||||
|
||||
private var shownCaps = [Cap]()
|
||||
|
||||
private var sortType: SortCriteria = .id
|
||||
|
||||
private var sortAscending: Bool = false
|
||||
|
||||
/// This will be set to a cap id when adding a cap to it
|
||||
private var capToAddImageTo: Cap?
|
||||
|
||||
// MARK: - Sort
|
||||
|
||||
/**
|
||||
Resets the cap list to its original state, discarding any
|
||||
previous sorting.
|
||||
*/
|
||||
private func showAllCapsByDescendingId() {
|
||||
sortType = .id
|
||||
sortAscending = false
|
||||
showAllCapsAndScrollToTop()
|
||||
}
|
||||
|
||||
/**
|
||||
Display all caps in the table, and scrolls to the top
|
||||
*/
|
||||
private func showAllCapsAndScrollToTop() {
|
||||
caps = Cap.capList(sortedBy: sortType, ascending: sortAscending)
|
||||
shownCaps = caps
|
||||
table.reloadData()
|
||||
tableViewScrollToTop()
|
||||
}
|
||||
|
||||
// MARK: - TableView
|
||||
|
||||
private func tableViewSetup() {
|
||||
table.dataSource = self
|
||||
table.delegate = self
|
||||
table.rowHeight = 100
|
||||
}
|
||||
|
||||
/**
|
||||
Scroll the table view to the top
|
||||
*/
|
||||
private func tableViewScrollToTop() {
|
||||
guard shownCaps.count > 0 else { return }
|
||||
let path = IndexPath(row: 0, section: 0)
|
||||
table.scrollToRow(at: path, at: .top, animated: true)
|
||||
}
|
||||
|
||||
private func rename(cap: Cap, at indexPath: IndexPath) {
|
||||
|
||||
let alertController = UIAlertController(
|
||||
title: "Enter name",
|
||||
message: "Choose a name for the image",
|
||||
preferredStyle: .alert,
|
||||
blurStyle: .dark)
|
||||
|
||||
alertController.addTextField { textField in
|
||||
textField.text = cap.name
|
||||
textField.placeholder = "Cap name"
|
||||
textField.keyboardType = .default
|
||||
}
|
||||
|
||||
let action = UIAlertAction(title: "Save", style: .default) { _ in
|
||||
guard let name = alertController.textFields?.first?.text else {
|
||||
return
|
||||
}
|
||||
cap.name = name
|
||||
self.table.reloadRows(at: [indexPath], with: .none)
|
||||
}
|
||||
alertController.addAction(action)
|
||||
|
||||
let cancel = UIAlertAction(title: "Cancel", style: .cancel)
|
||||
alertController.addAction(cancel)
|
||||
|
||||
self.present(alertController, animated: true)
|
||||
alertController.view.tintColor = AppDelegate.tintColor
|
||||
}
|
||||
|
||||
// MARK: - Search bar
|
||||
|
||||
private func searchBarSetup() {
|
||||
searchBar.text = nil
|
||||
searchBar.setShowsCancelButton(false, animated: false)
|
||||
registerKeyboardNotifications()
|
||||
}
|
||||
|
||||
private func setSearchBarTextColor() {
|
||||
for subView in searchBar.subviews {
|
||||
for view in subView.subviews {
|
||||
if let textView = view as? UITextField {
|
||||
textView.textColor = AppDelegate.tintColor
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func registerKeyboardNotifications() {
|
||||
NotificationCenter.default.addObserver(
|
||||
self, selector: #selector(TableView.animateWithKeyboard),
|
||||
name: UIResponder.keyboardWillShowNotification, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self, selector: #selector(TableView.animateWithKeyboard),
|
||||
name: UIResponder.keyboardWillHideNotification, object: nil)
|
||||
}
|
||||
|
||||
@objc func animateWithKeyboard(notification: NSNotification) {
|
||||
let userInfo = notification.userInfo!
|
||||
let keyboardHeight = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height
|
||||
let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! Double
|
||||
let curve = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! UInt
|
||||
let moveUp = (notification.name == UIResponder.keyboardWillShowNotification)
|
||||
|
||||
searchBarConstraint.constant = moveUp ? keyboardHeight + 1 : 0
|
||||
searchBar.setShowsCancelButton(moveUp, animated: false)
|
||||
imageButtonConstraint.constant = moveUp ? -56 : 0
|
||||
|
||||
let options = UIView.AnimationOptions(rawValue: curve << 16)
|
||||
|
||||
UIView.animate(withDuration: duration, delay: 0, options: options,
|
||||
animations: { self.view.layoutIfNeeded() })
|
||||
}
|
||||
|
||||
// MARK: - Image/clear button
|
||||
|
||||
@IBOutlet weak var imageClearButton: UIButton!
|
||||
|
||||
private func imageButtonSetup() {
|
||||
let tint = AppDelegate.tintColor
|
||||
imageClearButton.setImage(UIImage.templateImage(named: "camera_square"), for: .normal)
|
||||
imageClearButton.tintColor = tint
|
||||
}
|
||||
|
||||
@IBAction func imageClearButtonPressed() {
|
||||
discardImage()
|
||||
}
|
||||
|
||||
// MARK: - Image view
|
||||
|
||||
@IBOutlet weak var imageView: UIView!
|
||||
|
||||
@IBOutlet weak var imageViewContraint: NSLayoutConstraint!
|
||||
|
||||
private func discardImage() {
|
||||
searchBar.resignFirstResponder()
|
||||
searchBar.text = nil
|
||||
Cap.hasMatches = false
|
||||
showAllCapsByDescendingId()
|
||||
imageView(shouldBeVisible: false)
|
||||
}
|
||||
|
||||
private func showImageView(with image: UIImage) {
|
||||
newImage.image = image
|
||||
showImageView()
|
||||
}
|
||||
|
||||
private func imageView(shouldBeVisible: Bool) {
|
||||
shouldBeVisible ? showImageView() : dismissImageView()
|
||||
}
|
||||
|
||||
private func showImageView() {
|
||||
UIView.animate(withDuration: 0.5) {
|
||||
self.imageViewContraint.constant = 0
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissImageView() {
|
||||
UIView.animate(withDuration: 0.5, animations: {
|
||||
self.imageViewContraint.constant = self.imageViewDistance
|
||||
}) { _ in self.newImage.image = nil }
|
||||
}
|
||||
|
||||
@IBOutlet weak var newImage: RoundedImageView!
|
||||
|
||||
@IBOutlet weak var saveButton: RoundedButton!
|
||||
|
||||
@IBAction func saveButtonPressed() {
|
||||
if let image = newImage.image {
|
||||
saveNewCap(for: image)
|
||||
}
|
||||
}
|
||||
|
||||
private func saveNewCap(for image: UIImage) {
|
||||
|
||||
let alertController = UIAlertController(
|
||||
title: "Enter name",
|
||||
message: "Choose a name for the image",
|
||||
preferredStyle: .alert,
|
||||
blurStyle: .dark)
|
||||
|
||||
alertController.addTextField { textField in
|
||||
//textField.backgroundColor = UIColor.darkGray
|
||||
textField.textColor = AppDelegate.tintColor
|
||||
textField.placeholder = "Cap name"
|
||||
textField.keyboardType = .default
|
||||
}
|
||||
|
||||
let action = UIAlertAction(title: "Save", style: .default) { _ in
|
||||
guard let name = alertController.textFields?.first?.text else {
|
||||
self.showAlert("No name for cap")
|
||||
return
|
||||
}
|
||||
|
||||
let _ = Cap(image: image, name: name)
|
||||
self.discardImage()
|
||||
}
|
||||
|
||||
let cancel = UIAlertAction(title: "Cancel", style: .cancel)
|
||||
|
||||
alertController.addAction(action)
|
||||
alertController.addAction(cancel)
|
||||
self.present(alertController, animated: true)
|
||||
alertController.view.tintColor = AppDelegate.tintColor
|
||||
}
|
||||
|
||||
@IBOutlet weak var deleteButton: RoundedButton!
|
||||
|
||||
@IBAction func deleteButtonPressed() {
|
||||
discardImage()
|
||||
}
|
||||
|
||||
// MARK: - Navigation
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
self.giveFeedback(.medium)
|
||||
guard let id = segue.identifier else {
|
||||
error("No identifier for segue")
|
||||
return
|
||||
}
|
||||
switch id {
|
||||
case "showCamera":
|
||||
(segue.destination as! CameraController).delegate = self
|
||||
case "showSettings":
|
||||
break
|
||||
default:
|
||||
error("Invalid segue identifier \(id)")
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func showSortOptions(_ sender: UIBarButtonItem) {
|
||||
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
||||
let controller = storyboard.instantiateViewController(withIdentifier: "SortController") as! SortController
|
||||
controller.selected = sortType
|
||||
controller.ascending = sortAscending
|
||||
controller.delegate = self
|
||||
|
||||
let presentationController = AlwaysPresentAsPopover.configurePresentation(forController: controller)
|
||||
|
||||
presentationController.barButtonItem = sender
|
||||
presentationController.permittedArrowDirections = [.up]
|
||||
self.present(controller, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - SortControllerDelegate
|
||||
|
||||
extension TableView: SortControllerDelegate {
|
||||
|
||||
func sortController(didSelect sortType: SortCriteria, ascending: Bool) {
|
||||
self.sortType = sortType
|
||||
self.sortAscending = ascending
|
||||
if sortType != .match {
|
||||
Cap.hasMatches = false
|
||||
}
|
||||
showAllCapsAndScrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CameraControllerDelegate
|
||||
|
||||
extension TableView: CameraControllerDelegate {
|
||||
|
||||
func didCapture(image: UIImage) {
|
||||
if let cap = capToAddImageTo {
|
||||
cap.add(image: image) { success in
|
||||
guard success else {
|
||||
self.error("Could not save image")
|
||||
return
|
||||
}
|
||||
self.capToAddImageTo = nil
|
||||
}
|
||||
} else {
|
||||
// Hand image to classifier, delegate is ListViewController
|
||||
Classifier.shared.recognise(image: image)
|
||||
}
|
||||
}
|
||||
|
||||
func didCancel() {
|
||||
capToAddImageTo = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
extension TableView: UITableViewDataSource {
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "cap") as! CapCell
|
||||
let cap = shownCaps[indexPath.row]
|
||||
cell.cap = cap
|
||||
return cell
|
||||
}
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return shownCaps.count
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
|
||||
extension TableView: UITableViewDelegate {
|
||||
|
||||
private func takeImage(for cap: Cap) {
|
||||
self.capToAddImageTo = cap
|
||||
self.performSegue(withIdentifier: "showCamera", sender: nil)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let cap = shownCaps[indexPath.row]
|
||||
if let image = newImage.image {
|
||||
cap.add(image: image) { success in
|
||||
guard success else {
|
||||
self.error("Could not save image")
|
||||
return
|
||||
}
|
||||
self.giveFeedback(.medium)
|
||||
self.updateCell(for: cap.id)
|
||||
self.discardImage()
|
||||
}
|
||||
} else {
|
||||
self.giveFeedback(.medium)
|
||||
takeImage(for: cap)
|
||||
}
|
||||
table.deselectRow(at: indexPath, animated: true)
|
||||
}
|
||||
|
||||
private func updateCell(for capId: Int) {
|
||||
let cell = table.visibleCells.first { cell in
|
||||
let item = cell as! CapCell
|
||||
return item.cap.id == capId
|
||||
}
|
||||
(cell as! CapCell).updateCell()
|
||||
}
|
||||
|
||||
private func giveFeedback(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
|
||||
let generator = UIImpactFeedbackGenerator(style: style)
|
||||
generator.impactOccurred()
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let cap = shownCaps[indexPath.row]
|
||||
|
||||
let rename = UIContextualAction(style: .normal, title: "Rename\ncap") { (_, _, success) in
|
||||
success(true)
|
||||
self.rename(cap: cap, at: indexPath)
|
||||
self.giveFeedback(.medium)
|
||||
}
|
||||
rename.backgroundColor = .blue
|
||||
|
||||
let image = UIContextualAction(style: .normal, title: "Change\nimage") { (_, _, success) in
|
||||
self.giveFeedback(.medium)
|
||||
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
||||
let controller = storyboard.instantiateViewController(withIdentifier: "ImageSelector") as! ImageSelector
|
||||
controller.cap = cap
|
||||
self.navigationController?.pushViewController(controller, animated: true)
|
||||
success(true)
|
||||
}
|
||||
image.backgroundColor = .red
|
||||
|
||||
return UISwipeActionsConfiguration(actions: [rename, image])
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let cap = shownCaps[indexPath.row]
|
||||
|
||||
let count = UIContextualAction(style: .normal, title: "Update\ncount") { (_, _, success) in
|
||||
self.giveFeedback(.medium)
|
||||
cap.updateCount { result in
|
||||
self.updateCell(for: cap.id)
|
||||
success(result)
|
||||
}
|
||||
}
|
||||
count.backgroundColor = .orange
|
||||
|
||||
let similar = UIContextualAction(style: .normal, title: "Similar\ncaps") { (_, _, success) in
|
||||
self.giveFeedback(.medium)
|
||||
self.imageView(shouldBeVisible: false)
|
||||
if let image = cap.image {
|
||||
Classifier.shared.recognise(image: image, reportingImage: false)
|
||||
}
|
||||
success(true)
|
||||
}
|
||||
similar.backgroundColor = .blue
|
||||
|
||||
return UISwipeActionsConfiguration(actions: [similar, count])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ClassifierDelegate
|
||||
|
||||
extension TableView: ClassifierDelegate {
|
||||
|
||||
func classifier(finished image: UIImage?) {
|
||||
if let img = image {
|
||||
showImageView(with: img)
|
||||
}
|
||||
sortType = .match
|
||||
sortAscending = false
|
||||
Cap.hasMatches = true
|
||||
showAllCapsAndScrollToTop()
|
||||
}
|
||||
|
||||
func classifier(error: String) {
|
||||
self.showAlert(error)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UISearchBarDelegate
|
||||
|
||||
extension TableView: UISearchBarDelegate {
|
||||
|
||||
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||
searchBar.resignFirstResponder()
|
||||
searchBar.text = nil
|
||||
showAllCapsAndScrollToTop()
|
||||
}
|
||||
|
||||
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||
guard searchText != "" else {
|
||||
showAllCapsAndScrollToTop()
|
||||
return
|
||||
}
|
||||
DispatchQueue.global(qos: .userInteractive).async {
|
||||
let cleaned = searchText.clean
|
||||
let filteredCaps = self.caps.filter { cap in
|
||||
let name = cap.cleanName
|
||||
// For each part of text, check if name contains it
|
||||
for textItem in cleaned.components(separatedBy: " ") {
|
||||
if textItem != "" && !name.contains(textItem) { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
self.shownCaps = filteredCaps
|
||||
self.table.reloadData()
|
||||
self.tableViewScrollToTop()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Logging
|
||||
|
||||
extension TableView: Logger {
|
||||
|
||||
static let logToken = "[TableView]"
|
||||
}
|
||||
|
||||
// MARK: - Protocol CapsDelegate
|
||||
|
||||
extension TableView: CapsDelegate {
|
||||
|
||||
func capHasUpdates(_ cap: Cap) {
|
||||
if let cell = table.visibleCells.first(where: { ($0 as! CapCell).cap == cap }) {
|
||||
(cell as! CapCell).updateCell()
|
||||
} else if !caps.contains(cap) {
|
||||
// Reload the table when new cap is added
|
||||
showAllCapsAndScrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
func capsLoaded() {
|
||||
showAllCapsByDescendingId()
|
||||
}
|
||||
}
|
42
CapCollector/View Components/AlwaysShowPopup.swift
Normal file
42
CapCollector/View Components/AlwaysShowPopup.swift
Normal file
@ -0,0 +1,42 @@
|
||||
// Copyright 2018, Ralf Ebert
|
||||
// License https://opensource.org/licenses/MIT
|
||||
// License https://creativecommons.org/publicdomain/zero/1.0/
|
||||
// Source https://www.ralfebert.de/ios-examples/uikit/choicepopover/
|
||||
|
||||
import UIKit
|
||||
|
||||
/**
|
||||
By default, when you use:
|
||||
|
||||
```
|
||||
controller.modalPresentationStyle = .popover
|
||||
```
|
||||
|
||||
in a horizontally compact environment (iPhone in portrait mode), this option behaves the same as fullScreen.
|
||||
You can make it to always show a popover by using:
|
||||
|
||||
```
|
||||
let presentationController = AlwaysPresentAsPopover.configurePresentation(forController: controller)
|
||||
```
|
||||
*/
|
||||
class AlwaysPresentAsPopover : NSObject, UIPopoverPresentationControllerDelegate {
|
||||
|
||||
// `sharedInstance` because the delegate property is weak - the delegate instance needs to be retained.
|
||||
private static let sharedInstance = AlwaysPresentAsPopover()
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
|
||||
return .none
|
||||
}
|
||||
|
||||
static func configurePresentation(forController controller : UIViewController) -> UIPopoverPresentationController {
|
||||
controller.modalPresentationStyle = .popover
|
||||
let presentationController = controller.presentationController as! UIPopoverPresentationController
|
||||
presentationController.delegate = AlwaysPresentAsPopover.sharedInstance
|
||||
return presentationController
|
||||
}
|
||||
|
||||
}
|
52
CapCollector/View Components/CropView.swift
Normal file
52
CapCollector/View Components/CropView.swift
Normal file
@ -0,0 +1,52 @@
|
||||
//
|
||||
// CropView.swift
|
||||
// CapFinder
|
||||
//
|
||||
// Created by User on 31.01.18.
|
||||
// Copyright © 2018 User. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@IBDesignable class CropView: UIView {
|
||||
|
||||
@IBInspectable var lineColor: UIColor = UIColor.black
|
||||
|
||||
@IBInspectable var lineWidth: Float = 2
|
||||
|
||||
@IBInspectable var relativeSize: Float = 0.6 {
|
||||
didSet { size = CGFloat(relativeSize) / 2 }
|
||||
}
|
||||
|
||||
private var size: CGFloat = 0.3
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
}
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
let height = rect.height
|
||||
let width = rect.width
|
||||
|
||||
let length = height > width ? width : height
|
||||
|
||||
let center = CGPoint(x: width / 2, y: height / 2)
|
||||
let path = UIBezierPath()
|
||||
|
||||
path.lineWidth = CGFloat(self.lineWidth)
|
||||
lineColor.setStroke()
|
||||
|
||||
path.addArc(
|
||||
withCenter: center,
|
||||
radius: length * size,
|
||||
startAngle: 0,
|
||||
endAngle: .pi * 2,
|
||||
clockwise: true)
|
||||
|
||||
path.stroke()
|
||||
}
|
||||
}
|
Binary file not shown.
8
Podfile
Normal file
8
Podfile
Normal file
@ -0,0 +1,8 @@
|
||||
platform :ios, '11.0'
|
||||
use_frameworks!
|
||||
|
||||
target 'CapCollector' do
|
||||
|
||||
# Pods for CapFinder
|
||||
pod 'SwiftyDropbox'
|
||||
end
|
File diff suppressed because one or more lines are too long
@ -1,12 +1,15 @@
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/SettingsController.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/Classifier.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/SortController.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/Logger.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/DropBoxController.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/UIViewExtensions.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/NavigationController.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/Cap.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/DiskManager.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/UIImageExtensions.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/CapImages.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/TableView.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/GridViewController.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/NameFile.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/UserDefaults.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/AppDelegate.o
|
||||
@ -15,14 +18,13 @@
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/ViewControllerExtensions.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/PhotoCaptureHandler.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/RoundedButton.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/CapNames.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/ImageCell.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/UIAlertControllerExtensions.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/CameraView.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/CropView.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/DropboxDownload.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/AlwaysShowPopup.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/RoundedImageView.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/DropboxUpload.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/CapCell.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/Squeezenet.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/ImageClassifier.o
|
||||
/Users/christoph/Documents/GitHub/CapCollector/build/CapCollector/Build/Intermediates.noindex/CapCollector.build/Debug-iphoneos/CapCollector.build/Objects-normal/arm64/Resnet.o
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LastAccessedDate</key>
|
||||
<date>2018-10-17T09:43:52Z</date>
|
||||
<date>2019-03-15T12:04:14Z</date>
|
||||
<key>WorkspacePath</key>
|
||||
<string>/Users/christoph/Documents/GitHub/CapCollector/CapCollector.xcworkspace</string>
|
||||
</dict>
|
||||
|
@ -2,8 +2,6 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SuppressMissingCheckoutsAlert</key>
|
||||
<true/>
|
||||
<key>Version</key>
|
||||
<integer>5</integer>
|
||||
</dict>
|
||||
|
@ -1 +1 @@
|
||||
1539769432.274418: Module build session file for module cache at Path(str: "/Users/christoph/Documents/GitHub/CapCollector/build/ModuleCache.noindex")
|
||||
1552651454.561245: Module build session file for module cache at Path(str: "/Users/christoph/Documents/GitHub/CapCollector/build/ModuleCache.noindex")
|
||||
|
Loading…
Reference in New Issue
Block a user