Download classifier, database
This commit is contained in:
parent
dceb3ca07d
commit
7287607a60
@ -3,59 +3,60 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 50;
|
objectVersion = 52;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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 */; };
|
5904C33A2199C9FA0046A573 /* SortController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5904C3392199C9FA0046A573 /* SortController.swift */; };
|
||||||
5904C33C2199D0260046A573 /* AlwaysShowPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */; };
|
5904C33C2199D0260046A573 /* AlwaysShowPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */; };
|
||||||
59158B1621E37B0200D90CB0 /* GridViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59158B1521E37B0200D90CB0 /* GridViewController.swift */; };
|
59158B1621E37B0200D90CB0 /* GridViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59158B1521E37B0200D90CB0 /* GridViewController.swift */; };
|
||||||
59158B1821E4C9AC00D90CB0 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59158B1721E4C9AC00D90CB0 /* NavigationController.swift */; };
|
59158B1821E4C9AC00D90CB0 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59158B1721E4C9AC00D90CB0 /* NavigationController.swift */; };
|
||||||
591832CE21A2A97E00E5987D /* Cap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591832CD21A2A97E00E5987D /* Cap.swift */; };
|
591832CE21A2A97E00E5987D /* Cap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591832CD21A2A97E00E5987D /* Cap.swift */; };
|
||||||
|
591FDD1E234E151600AA379E /* SearchAndDisplayAccessory.xib in Resources */ = {isa = PBXBuildFile; fileRef = 591FDD1D234E151600AA379E /* SearchAndDisplayAccessory.xib */; };
|
||||||
|
591FDD20234E162000AA379E /* SearchAndDisplayAccessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591FDD1F234E162000AA379E /* SearchAndDisplayAccessory.swift */; };
|
||||||
5970380D225737F800D21B55 /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5970380C225737F800D21B55 /* LogViewController.swift */; };
|
5970380D225737F800D21B55 /* LogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5970380C225737F800D21B55 /* LogViewController.swift */; };
|
||||||
599BC9DA22CBBDA90061BCDB /* Squeezenet.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = 599BC9D922CBBDA90061BCDB /* Squeezenet.mlmodel */; };
|
|
||||||
599BC9DC22CDE2640061BCDB /* MobileNet.mlmodel in Sources */ = {isa = PBXBuildFile; fileRef = 599BC9DB22CDE2640061BCDB /* MobileNet.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 */; };
|
CE56CECE209D81DE00932C01 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CECD209D81DE00932C01 /* AppDelegate.swift */; };
|
||||||
CE56CED3209D81DE00932C01 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE56CED1209D81DE00932C01 /* Main.storyboard */; };
|
CE56CED3209D81DE00932C01 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE56CED1209D81DE00932C01 /* Main.storyboard */; };
|
||||||
CE56CED5209D81E000932C01 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE56CED4209D81E000932C01 /* Assets.xcassets */; };
|
CE56CED5209D81E000932C01 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE56CED4209D81E000932C01 /* Assets.xcassets */; };
|
||||||
CE56CED8209D81E000932C01 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE56CED6209D81E000932C01 /* LaunchScreen.storyboard */; };
|
CE56CED8209D81E000932C01 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE56CED6209D81E000932C01 /* LaunchScreen.storyboard */; };
|
||||||
CE56CEF8209D83B800932C01 /* CapCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE0209D83B200932C01 /* CapCell.swift */; };
|
CE56CEF8209D83B800932C01 /* CapCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE0209D83B200932C01 /* CapCell.swift */; };
|
||||||
CE56CEFD209D83B800932C01 /* NameFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE5209D83B300932C01 /* NameFile.swift */; };
|
|
||||||
CE56CEFE209D83B800932C01 /* RoundedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE6209D83B300932C01 /* RoundedButton.swift */; };
|
CE56CEFE209D83B800932C01 /* RoundedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE6209D83B300932C01 /* RoundedButton.swift */; };
|
||||||
CE56CEFF209D83B800932C01 /* CameraController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE7209D83B300932C01 /* CameraController.swift */; };
|
CE56CEFF209D83B800932C01 /* CameraController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE7209D83B300932C01 /* CameraController.swift */; };
|
||||||
CE56CF00209D83B800932C01 /* UIAlertControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE8209D83B300932C01 /* UIAlertControllerExtensions.swift */; };
|
|
||||||
CE56CF01209D83B800932C01 /* DropBoxController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE9209D83B400932C01 /* DropBoxController.swift */; };
|
|
||||||
CE56CF02209D83B800932C01 /* RoundedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEA209D83B400932C01 /* RoundedImageView.swift */; };
|
CE56CF02209D83B800932C01 /* RoundedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEA209D83B400932C01 /* RoundedImageView.swift */; };
|
||||||
CE56CF03209D83B800932C01 /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEB209D83B400932C01 /* TableView.swift */; };
|
CE56CF03209D83B800932C01 /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEB209D83B400932C01 /* TableView.swift */; };
|
||||||
CE56CF04209D83B800932C01 /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEC209D83B400932C01 /* UIViewExtensions.swift */; };
|
CE56CF04209D83B800932C01 /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEC209D83B400932C01 /* UIViewExtensions.swift */; };
|
||||||
CE56CF05209D83B800932C01 /* ViewControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEED209D83B400932C01 /* ViewControllerExtensions.swift */; };
|
CE56CF05209D83B800932C01 /* ViewControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEED209D83B400932C01 /* ViewControllerExtensions.swift */; };
|
||||||
CE56CF06209D83B800932C01 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEE209D83B500932C01 /* CameraView.swift */; };
|
CE56CF06209D83B800932C01 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEE209D83B500932C01 /* CameraView.swift */; };
|
||||||
CE56CF07209D83B800932C01 /* ImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEF209D83B500932C01 /* ImageCell.swift */; };
|
CE56CF07209D83B800932C01 /* ImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEF209D83B500932C01 /* ImageCell.swift */; };
|
||||||
CE56CF08209D83B800932C01 /* DiskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF0209D83B500932C01 /* DiskManager.swift */; };
|
CE56CF08209D83B800932C01 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF0209D83B500932C01 /* Storage.swift */; };
|
||||||
CE56CF09209D83B800932C01 /* Classifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF1209D83B500932C01 /* Classifier.swift */; };
|
CE56CF09209D83B800932C01 /* Classifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF1209D83B500932C01 /* Classifier.swift */; };
|
||||||
CE56CF0A209D83B800932C01 /* CropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF2209D83B600932C01 /* CropView.swift */; };
|
CE56CF0A209D83B800932C01 /* CropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF2209D83B600932C01 /* CropView.swift */; };
|
||||||
CE56CF0B209D83B800932C01 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF3209D83B600932C01 /* Logger.swift */; };
|
CE56CF0B209D83B800932C01 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF3209D83B600932C01 /* Logger.swift */; };
|
||||||
CE56CF0D209D83B800932C01 /* ImageSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF5209D83B600932C01 /* ImageSelector.swift */; };
|
CE56CF0D209D83B800932C01 /* ImageSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF5209D83B600932C01 /* ImageSelector.swift */; };
|
||||||
CE56CF0E209D83B800932C01 /* PhotoCaptureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF6209D83B700932C01 /* PhotoCaptureHandler.swift */; };
|
CE56CF0E209D83B800932C01 /* PhotoCaptureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF6209D83B700932C01 /* PhotoCaptureHandler.swift */; };
|
||||||
CE56CF0F209D83B800932C01 /* UIImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF7209D83B700932C01 /* UIImageExtensions.swift */; };
|
CE56CF0F209D83B800932C01 /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF7209D83B700932C01 /* UIImage+Extensions.swift */; };
|
||||||
|
CE5B7CFC24562673002E5C06 /* Download.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5B7CFB24562673002E5C06 /* Download.swift */; };
|
||||||
|
CE5B7CFE245626D3002E5C06 /* Upload.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5B7CFD245626D3002E5C06 /* Upload.swift */; };
|
||||||
|
CE5B7D0024574CCA002E5C06 /* ServerDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5B7CFF24574CCA002E5C06 /* ServerDatabase.swift */; };
|
||||||
|
CE5B7D032458C921002E5C06 /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = CE5B7D022458C921002E5C06 /* Reachability */; };
|
||||||
|
CE6E4828246C304100570CB0 /* CGImagePropertyOrientation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6E4827246C304100570CB0 /* CGImagePropertyOrientation+Extensions.swift */; };
|
||||||
|
CE85AA16246A96C3002D1074 /* UINavigationItem+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE85AA15246A96C3002D1074 /* UINavigationItem+Extensions.swift */; };
|
||||||
|
CE85AA18246B012B002D1074 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE85AA17246B012B002D1074 /* Array+Extensions.swift */; };
|
||||||
|
CEB269572445DB56004B74B3 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = CEB269562445DB56004B74B3 /* SQLite */; };
|
||||||
|
CEB269592445DB72004B74B3 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB269582445DB72004B74B3 /* Database.swift */; };
|
||||||
|
CEB2695B2445E54E004B74B3 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB2695A2445E54E004B74B3 /* UIColor+Extensions.swift */; };
|
||||||
|
CEC7F815245A2B1200B896B1 /* JGProgressHUD in Frameworks */ = {isa = PBXBuildFile; productRef = CEC7F814245A2B1200B896B1 /* JGProgressHUD */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference 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>"; };
|
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>"; };
|
5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlwaysShowPopup.swift; sourceTree = "<group>"; };
|
||||||
59158B1521E37B0200D90CB0 /* GridViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridViewController.swift; 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>"; };
|
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>"; };
|
591832CD21A2A97E00E5987D /* Cap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cap.swift; sourceTree = "<group>"; };
|
||||||
|
591FDD1D234E151600AA379E /* SearchAndDisplayAccessory.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SearchAndDisplayAccessory.xib; sourceTree = "<group>"; };
|
||||||
|
591FDD1F234E162000AA379E /* SearchAndDisplayAccessory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchAndDisplayAccessory.swift; sourceTree = "<group>"; };
|
||||||
5970380C225737F800D21B55 /* LogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = "<group>"; };
|
5970380C225737F800D21B55 /* LogViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogViewController.swift; sourceTree = "<group>"; };
|
||||||
599BC9D922CBBDA90061BCDB /* Squeezenet.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = Squeezenet.mlmodel; path = ../../../../Dropbox/Models/Squeezenet.mlmodel; sourceTree = "<group>"; };
|
|
||||||
599BC9DB22CDE2640061BCDB /* MobileNet.mlmodel */ = {isa = PBXFileReference; lastKnownFileType = file.mlmodel; name = MobileNet.mlmodel; path = ../../../../Dropbox/Models/MobileNet.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; };
|
|
||||||
CE56CECA209D81DD00932C01 /* CapCollector.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CapCollector.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
CE56CECA209D81DD00932C01 /* CapCollector.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CapCollector.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
CE56CECD209D81DE00932C01 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
CE56CECD209D81DE00932C01 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
CE56CED2209D81DE00932C01 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
CE56CED2209D81DE00932C01 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
@ -63,25 +64,30 @@
|
|||||||
CE56CED7209D81E000932C01 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
CE56CEE0209D83B200932C01 /* CapCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CapCell.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>"; };
|
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>"; };
|
CE56CEE7209D83B300932C01 /* CameraController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraController.swift; sourceTree = "<group>"; };
|
||||||
CE56CEE8209D83B300932C01 /* UIAlertControllerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAlertControllerExtensions.swift; sourceTree = "<group>"; };
|
CE56CEE8209D83B300932C01 /* UIAlertControllerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAlertControllerExtensions.swift; sourceTree = "<group>"; };
|
||||||
CE56CEE9209D83B400932C01 /* DropBoxController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropBoxController.swift; sourceTree = "<group>"; };
|
|
||||||
CE56CEEA209D83B400932C01 /* RoundedImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedImageView.swift; sourceTree = "<group>"; };
|
CE56CEEA209D83B400932C01 /* RoundedImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedImageView.swift; sourceTree = "<group>"; };
|
||||||
CE56CEEB209D83B400932C01 /* TableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TableView.swift; path = ../TableView.swift; sourceTree = "<group>"; };
|
CE56CEEB209D83B400932C01 /* TableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TableView.swift; path = ../TableView.swift; sourceTree = "<group>"; };
|
||||||
CE56CEEC209D83B400932C01 /* UIViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = "<group>"; };
|
CE56CEEC209D83B400932C01 /* UIViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = "<group>"; };
|
||||||
CE56CEED209D83B400932C01 /* ViewControllerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerExtensions.swift; sourceTree = "<group>"; };
|
CE56CEED209D83B400932C01 /* ViewControllerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerExtensions.swift; sourceTree = "<group>"; };
|
||||||
CE56CEEE209D83B500932C01 /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
|
CE56CEEE209D83B500932C01 /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
|
||||||
CE56CEEF209D83B500932C01 /* ImageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCell.swift; sourceTree = "<group>"; };
|
CE56CEEF209D83B500932C01 /* ImageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCell.swift; sourceTree = "<group>"; };
|
||||||
CE56CEF0209D83B500932C01 /* DiskManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiskManager.swift; sourceTree = "<group>"; };
|
CE56CEF0209D83B500932C01 /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
|
||||||
CE56CEF1209D83B500932C01 /* Classifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Classifier.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
CE56CEF3209D83B600932C01 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||||
CE56CEF5209D83B600932C01 /* ImageSelector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageSelector.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>"; };
|
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>"; };
|
CE56CEF7209D83B700932C01 /* UIImage+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Extensions.swift"; 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>"; };
|
CE5B7CFB24562673002E5C06 /* Download.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Download.swift; sourceTree = "<group>"; };
|
||||||
|
CE5B7CFD245626D3002E5C06 /* Upload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Upload.swift; sourceTree = "<group>"; };
|
||||||
|
CE5B7CFF24574CCA002E5C06 /* ServerDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerDatabase.swift; sourceTree = "<group>"; };
|
||||||
|
CE6E4827246C304100570CB0 /* CGImagePropertyOrientation+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGImagePropertyOrientation+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
CE85AA15246A96C3002D1074 /* UINavigationItem+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationItem+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
CE85AA17246B012B002D1074 /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
CEB269582445DB72004B74B3 /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = "<group>"; };
|
||||||
|
CEB2695A2445E54E004B74B3 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -89,37 +95,20 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
043EC7C35065DD26F6BB496F /* Pods_CapCollector.framework in Frameworks */,
|
CEB269572445DB56004B74B3 /* SQLite in Frameworks */,
|
||||||
|
CEC7F815245A2B1200B896B1 /* JGProgressHUD in Frameworks */,
|
||||||
|
CE5B7D032458C921002E5C06 /* Reachability in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
0FE92EFB7AA01ED92CDE6BF3 /* Pods */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
DBD72193E502C23E06DA913D /* Pods-CapCollector.debug.xcconfig */,
|
|
||||||
342A23CD7996DA1E7039C097 /* Pods-CapCollector.release.xcconfig */,
|
|
||||||
);
|
|
||||||
name = Pods;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
9EAE4B3CEE704AF443897B44 /* Frameworks */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
86546C4DAB5E47A540F6E8DD /* Pods_CapCollector.framework */,
|
|
||||||
);
|
|
||||||
name = Frameworks;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
CE56CEC1209D81DD00932C01 = {
|
CE56CEC1209D81DD00932C01 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
CE56CECC209D81DD00932C01 /* CapCollector */,
|
CE56CECC209D81DD00932C01 /* CapCollector */,
|
||||||
CE56CECB209D81DD00932C01 /* Products */,
|
CE56CECB209D81DD00932C01 /* Products */,
|
||||||
0FE92EFB7AA01ED92CDE6BF3 /* Pods */,
|
|
||||||
9EAE4B3CEE704AF443897B44 /* Frameworks */,
|
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@ -136,11 +125,7 @@
|
|||||||
children = (
|
children = (
|
||||||
CE56CECD209D81DE00932C01 /* AppDelegate.swift */,
|
CE56CECD209D81DE00932C01 /* AppDelegate.swift */,
|
||||||
CE56CED1209D81DE00932C01 /* Main.storyboard */,
|
CE56CED1209D81DE00932C01 /* Main.storyboard */,
|
||||||
CE56CEF1209D83B500932C01 /* Classifier.swift */,
|
|
||||||
599BC9D922CBBDA90061BCDB /* Squeezenet.mlmodel */,
|
|
||||||
599BC9DB22CDE2640061BCDB /* MobileNet.mlmodel */,
|
|
||||||
CEF3874D209D9378001C8D3C /* Capture */,
|
CEF3874D209D9378001C8D3C /* Capture */,
|
||||||
CEF3874E209D9390001C8D3C /* Sync */,
|
|
||||||
CEF38750209D93D1001C8D3C /* Data */,
|
CEF38750209D93D1001C8D3C /* Data */,
|
||||||
CEF3874B209D932E001C8D3C /* View Components */,
|
CEF3874B209D932E001C8D3C /* View Components */,
|
||||||
CEF3874F209D93A6001C8D3C /* Presentation */,
|
CEF3874F209D93A6001C8D3C /* Presentation */,
|
||||||
@ -176,7 +161,11 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
CE56CEE8209D83B300932C01 /* UIAlertControllerExtensions.swift */,
|
CE56CEE8209D83B300932C01 /* UIAlertControllerExtensions.swift */,
|
||||||
CE56CEF7209D83B700932C01 /* UIImageExtensions.swift */,
|
CE6E4827246C304100570CB0 /* CGImagePropertyOrientation+Extensions.swift */,
|
||||||
|
CE85AA17246B012B002D1074 /* Array+Extensions.swift */,
|
||||||
|
CE85AA15246A96C3002D1074 /* UINavigationItem+Extensions.swift */,
|
||||||
|
CEB2695A2445E54E004B74B3 /* UIColor+Extensions.swift */,
|
||||||
|
CE56CEF7209D83B700932C01 /* UIImage+Extensions.swift */,
|
||||||
CE56CEEC209D83B400932C01 /* UIViewExtensions.swift */,
|
CE56CEEC209D83B400932C01 /* UIViewExtensions.swift */,
|
||||||
CE56CEED209D83B400932C01 /* ViewControllerExtensions.swift */,
|
CE56CEED209D83B400932C01 /* ViewControllerExtensions.swift */,
|
||||||
);
|
);
|
||||||
@ -193,23 +182,16 @@
|
|||||||
path = Capture;
|
path = Capture;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
CEF3874E209D9390001C8D3C /* Sync */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
CE56CEE9209D83B400932C01 /* DropBoxController.swift */,
|
|
||||||
);
|
|
||||||
path = Sync;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
CEF3874F209D93A6001C8D3C /* Presentation */ = {
|
CEF3874F209D93A6001C8D3C /* Presentation */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
CE56CEEB209D83B400932C01 /* TableView.swift */,
|
CE56CEEB209D83B400932C01 /* TableView.swift */,
|
||||||
|
591FDD1D234E151600AA379E /* SearchAndDisplayAccessory.xib */,
|
||||||
|
591FDD1F234E162000AA379E /* SearchAndDisplayAccessory.swift */,
|
||||||
59158B1721E4C9AC00D90CB0 /* NavigationController.swift */,
|
59158B1721E4C9AC00D90CB0 /* NavigationController.swift */,
|
||||||
CE56CEE0209D83B200932C01 /* CapCell.swift */,
|
CE56CEE0209D83B200932C01 /* CapCell.swift */,
|
||||||
CE56CEF5209D83B600932C01 /* ImageSelector.swift */,
|
CE56CEF5209D83B600932C01 /* ImageSelector.swift */,
|
||||||
CE56CEEF209D83B500932C01 /* ImageCell.swift */,
|
CE56CEEF209D83B500932C01 /* ImageCell.swift */,
|
||||||
59C1BBA82174CBB800EC84BB /* SettingsController.swift */,
|
|
||||||
5904C3392199C9FA0046A573 /* SortController.swift */,
|
5904C3392199C9FA0046A573 /* SortController.swift */,
|
||||||
59158B1521E37B0200D90CB0 /* GridViewController.swift */,
|
59158B1521E37B0200D90CB0 /* GridViewController.swift */,
|
||||||
5970380C225737F800D21B55 /* LogViewController.swift */,
|
5970380C225737F800D21B55 /* LogViewController.swift */,
|
||||||
@ -220,10 +202,13 @@
|
|||||||
CEF38750209D93D1001C8D3C /* Data */ = {
|
CEF38750209D93D1001C8D3C /* Data */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
59C1BBAA21762D9600EC84BB /* UserDefaults.swift */,
|
CE56CEF1209D83B500932C01 /* Classifier.swift */,
|
||||||
591832CD21A2A97E00E5987D /* Cap.swift */,
|
591832CD21A2A97E00E5987D /* Cap.swift */,
|
||||||
CE56CEF0209D83B500932C01 /* DiskManager.swift */,
|
CE56CEF0209D83B500932C01 /* Storage.swift */,
|
||||||
CE56CEE5209D83B300932C01 /* NameFile.swift */,
|
CE5B7CFB24562673002E5C06 /* Download.swift */,
|
||||||
|
CE5B7CFD245626D3002E5C06 /* Upload.swift */,
|
||||||
|
CEB269582445DB72004B74B3 /* Database.swift */,
|
||||||
|
CE5B7CFF24574CCA002E5C06 /* ServerDatabase.swift */,
|
||||||
);
|
);
|
||||||
path = Data;
|
path = Data;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -235,17 +220,20 @@
|
|||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = CE56CEDC209D81E000932C01 /* Build configuration list for PBXNativeTarget "CapCollector" */;
|
buildConfigurationList = CE56CEDC209D81E000932C01 /* Build configuration list for PBXNativeTarget "CapCollector" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
3E8A16B5B7B80A2451075442 /* [CP] Check Pods Manifest.lock */,
|
|
||||||
CE56CEC6209D81DD00932C01 /* Sources */,
|
CE56CEC6209D81DD00932C01 /* Sources */,
|
||||||
CE56CEC7209D81DD00932C01 /* Frameworks */,
|
CE56CEC7209D81DD00932C01 /* Frameworks */,
|
||||||
CE56CEC8209D81DD00932C01 /* Resources */,
|
CE56CEC8209D81DD00932C01 /* Resources */,
|
||||||
135644FD94500ABE4DA09082 /* [CP] Embed Pods Frameworks */,
|
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = CapCollector;
|
name = CapCollector;
|
||||||
|
packageProductDependencies = (
|
||||||
|
CEB269562445DB56004B74B3 /* SQLite */,
|
||||||
|
CE5B7D022458C921002E5C06 /* Reachability */,
|
||||||
|
CEC7F814245A2B1200B896B1 /* JGProgressHUD */,
|
||||||
|
);
|
||||||
productName = CapCollector;
|
productName = CapCollector;
|
||||||
productReference = CE56CECA209D81DD00932C01 /* CapCollector.app */;
|
productReference = CE56CECA209D81DD00932C01 /* CapCollector.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
@ -262,7 +250,7 @@
|
|||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
CE56CEC9209D81DD00932C01 = {
|
CE56CEC9209D81DD00932C01 = {
|
||||||
CreatedOnToolsVersion = 9.4;
|
CreatedOnToolsVersion = 9.4;
|
||||||
LastSwiftMigration = 1020;
|
LastSwiftMigration = 1100;
|
||||||
SystemCapabilities = {
|
SystemCapabilities = {
|
||||||
com.apple.BackgroundModes = {
|
com.apple.BackgroundModes = {
|
||||||
enabled = 0;
|
enabled = 0;
|
||||||
@ -280,6 +268,11 @@
|
|||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = CE56CEC1209D81DD00932C01;
|
mainGroup = CE56CEC1209D81DD00932C01;
|
||||||
|
packageReferences = (
|
||||||
|
CEB269552445DB56004B74B3 /* XCRemoteSwiftPackageReference "SQLite" */,
|
||||||
|
CE5B7D012458C921002E5C06 /* XCRemoteSwiftPackageReference "Reachability" */,
|
||||||
|
CEC7F813245A2B1200B896B1 /* XCRemoteSwiftPackageReference "JGProgressHUD" */,
|
||||||
|
);
|
||||||
productRefGroup = CE56CECB209D81DD00932C01 /* Products */;
|
productRefGroup = CE56CECB209D81DD00932C01 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
@ -295,6 +288,7 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
CE56CED8209D81E000932C01 /* LaunchScreen.storyboard in Resources */,
|
CE56CED8209D81E000932C01 /* LaunchScreen.storyboard in Resources */,
|
||||||
|
591FDD1E234E151600AA379E /* SearchAndDisplayAccessory.xib in Resources */,
|
||||||
CE56CED5209D81E000932C01 /* Assets.xcassets in Resources */,
|
CE56CED5209D81E000932C01 /* Assets.xcassets in Resources */,
|
||||||
CE56CED3209D81DE00932C01 /* Main.storyboard in Resources */,
|
CE56CED3209D81DE00932C01 /* Main.storyboard in Resources */,
|
||||||
);
|
);
|
||||||
@ -302,82 +296,43 @@
|
|||||||
};
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
|
||||||
135644FD94500ABE4DA09082 /* [CP] Embed Pods Frameworks */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
"${SRCROOT}/Pods/Target Support Files/Pods-CapCollector/Pods-CapCollector-frameworks.sh",
|
|
||||||
"${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework",
|
|
||||||
"${BUILT_PRODUCTS_DIR}/SwiftyDropbox/SwiftyDropbox.framework",
|
|
||||||
);
|
|
||||||
name = "[CP] Embed Pods Frameworks";
|
|
||||||
outputPaths = (
|
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework",
|
|
||||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyDropbox.framework",
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CapCollector/Pods-CapCollector-frameworks.sh\"\n";
|
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
|
||||||
3E8A16B5B7B80A2451075442 /* [CP] Check Pods Manifest.lock */ = {
|
|
||||||
isa = PBXShellScriptBuildPhase;
|
|
||||||
buildActionMask = 2147483647;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
inputPaths = (
|
|
||||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
|
||||||
"${PODS_ROOT}/Manifest.lock",
|
|
||||||
);
|
|
||||||
name = "[CP] Check Pods Manifest.lock";
|
|
||||||
outputPaths = (
|
|
||||||
"$(DERIVED_FILE_DIR)/Pods-CapCollector-checkManifestLockResult.txt",
|
|
||||||
);
|
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
|
||||||
shellPath = /bin/sh;
|
|
||||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
|
||||||
showEnvVarsInLog = 0;
|
|
||||||
};
|
|
||||||
/* End PBXShellScriptBuildPhase section */
|
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
CE56CEC6209D81DD00932C01 /* Sources */ = {
|
CE56CEC6209D81DD00932C01 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
59C1BBA92174CBB800EC84BB /* SettingsController.swift in Sources */,
|
|
||||||
CE56CF09209D83B800932C01 /* Classifier.swift in Sources */,
|
CE56CF09209D83B800932C01 /* Classifier.swift in Sources */,
|
||||||
5904C33A2199C9FA0046A573 /* SortController.swift in Sources */,
|
5904C33A2199C9FA0046A573 /* SortController.swift in Sources */,
|
||||||
CE56CF0B209D83B800932C01 /* Logger.swift in Sources */,
|
CE56CF0B209D83B800932C01 /* Logger.swift in Sources */,
|
||||||
CE56CF01209D83B800932C01 /* DropBoxController.swift in Sources */,
|
|
||||||
CE56CF04209D83B800932C01 /* UIViewExtensions.swift in Sources */,
|
CE56CF04209D83B800932C01 /* UIViewExtensions.swift in Sources */,
|
||||||
59158B1821E4C9AC00D90CB0 /* NavigationController.swift in Sources */,
|
59158B1821E4C9AC00D90CB0 /* NavigationController.swift in Sources */,
|
||||||
591832CE21A2A97E00E5987D /* Cap.swift in Sources */,
|
591832CE21A2A97E00E5987D /* Cap.swift in Sources */,
|
||||||
CE56CF08209D83B800932C01 /* DiskManager.swift in Sources */,
|
CE56CF08209D83B800932C01 /* Storage.swift in Sources */,
|
||||||
CE56CF0F209D83B800932C01 /* UIImageExtensions.swift in Sources */,
|
CE56CF0F209D83B800932C01 /* UIImage+Extensions.swift in Sources */,
|
||||||
|
CE5B7CFC24562673002E5C06 /* Download.swift in Sources */,
|
||||||
|
CE85AA18246B012B002D1074 /* Array+Extensions.swift in Sources */,
|
||||||
CE56CF03209D83B800932C01 /* TableView.swift in Sources */,
|
CE56CF03209D83B800932C01 /* TableView.swift in Sources */,
|
||||||
|
CEB2695B2445E54E004B74B3 /* UIColor+Extensions.swift in Sources */,
|
||||||
|
591FDD20234E162000AA379E /* SearchAndDisplayAccessory.swift in Sources */,
|
||||||
59158B1621E37B0200D90CB0 /* GridViewController.swift in Sources */,
|
59158B1621E37B0200D90CB0 /* GridViewController.swift in Sources */,
|
||||||
CE56CEFD209D83B800932C01 /* NameFile.swift in Sources */,
|
CE5B7CFE245626D3002E5C06 /* Upload.swift in Sources */,
|
||||||
59C1BBAB21762D9600EC84BB /* UserDefaults.swift in Sources */,
|
|
||||||
CE56CECE209D81DE00932C01 /* AppDelegate.swift in Sources */,
|
CE56CECE209D81DE00932C01 /* AppDelegate.swift in Sources */,
|
||||||
CE56CF0D209D83B800932C01 /* ImageSelector.swift in Sources */,
|
CE56CF0D209D83B800932C01 /* ImageSelector.swift in Sources */,
|
||||||
CE56CEFF209D83B800932C01 /* CameraController.swift in Sources */,
|
CE56CEFF209D83B800932C01 /* CameraController.swift in Sources */,
|
||||||
|
CE5B7D0024574CCA002E5C06 /* ServerDatabase.swift in Sources */,
|
||||||
CE56CF05209D83B800932C01 /* ViewControllerExtensions.swift in Sources */,
|
CE56CF05209D83B800932C01 /* ViewControllerExtensions.swift in Sources */,
|
||||||
5970380D225737F800D21B55 /* LogViewController.swift in Sources */,
|
5970380D225737F800D21B55 /* LogViewController.swift in Sources */,
|
||||||
CE56CF0E209D83B800932C01 /* PhotoCaptureHandler.swift in Sources */,
|
CE56CF0E209D83B800932C01 /* PhotoCaptureHandler.swift in Sources */,
|
||||||
CE56CEFE209D83B800932C01 /* RoundedButton.swift in Sources */,
|
CE56CEFE209D83B800932C01 /* RoundedButton.swift in Sources */,
|
||||||
CE56CF07209D83B800932C01 /* ImageCell.swift in Sources */,
|
CE56CF07209D83B800932C01 /* ImageCell.swift in Sources */,
|
||||||
CE56CF00209D83B800932C01 /* UIAlertControllerExtensions.swift in Sources */,
|
|
||||||
CE56CF06209D83B800932C01 /* CameraView.swift in Sources */,
|
CE56CF06209D83B800932C01 /* CameraView.swift in Sources */,
|
||||||
CE56CF0A209D83B800932C01 /* CropView.swift in Sources */,
|
CE56CF0A209D83B800932C01 /* CropView.swift in Sources */,
|
||||||
5904C33C2199D0260046A573 /* AlwaysShowPopup.swift in Sources */,
|
5904C33C2199D0260046A573 /* AlwaysShowPopup.swift in Sources */,
|
||||||
599BC9DC22CDE2640061BCDB /* MobileNet.mlmodel in Sources */,
|
CE85AA16246A96C3002D1074 /* UINavigationItem+Extensions.swift in Sources */,
|
||||||
599BC9DA22CBBDA90061BCDB /* Squeezenet.mlmodel in Sources */,
|
CEB269592445DB72004B74B3 /* Database.swift in Sources */,
|
||||||
CE56CF02209D83B800932C01 /* RoundedImageView.swift in Sources */,
|
CE56CF02209D83B800932C01 /* RoundedImageView.swift in Sources */,
|
||||||
CE56CEF8209D83B800932C01 /* CapCell.swift in Sources */,
|
CE56CEF8209D83B800932C01 /* CapCell.swift in Sources */,
|
||||||
|
CE6E4828246C304100570CB0 /* CGImagePropertyOrientation+Extensions.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -519,44 +474,46 @@
|
|||||||
};
|
};
|
||||||
CE56CEDD209D81E000932C01 /* Debug */ = {
|
CE56CEDD209D81E000932C01 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = DBD72193E502C23E06DA913D /* Pods-CapCollector.debug.xcconfig */;
|
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||||
INFOPLIST_FILE = CapCollector/Info.plist;
|
INFOPLIST_FILE = CapCollector/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
|
MARKETING_VERSION = 1.4;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = christophhagen.CapCollector;
|
PRODUCT_BUNDLE_IDENTIFIER = christophhagen.CapCollector;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE = "";
|
PROVISIONING_PROFILE = "";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_VERSION = 4.2;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
CE56CEDE209D81E000932C01 /* Release */ = {
|
CE56CEDE209D81E000932C01 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 342A23CD7996DA1E7039C097 /* Pods-CapCollector.release.xcconfig */;
|
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||||
INFOPLIST_FILE = CapCollector/Info.plist;
|
INFOPLIST_FILE = CapCollector/Info.plist;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
|
MARKETING_VERSION = 1.4;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = christophhagen.CapCollector;
|
PRODUCT_BUNDLE_IDENTIFIER = christophhagen.CapCollector;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE = "";
|
PROVISIONING_PROFILE = "";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_VERSION = 4.2;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@ -583,6 +540,51 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
CE5B7D012458C921002E5C06 /* XCRemoteSwiftPackageReference "Reachability" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/ashleymills/Reachability.swift";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 5.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
CEB269552445DB56004B74B3 /* XCRemoteSwiftPackageReference "SQLite" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/stephencelis/SQLite.swift";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 0.12.2;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
CEC7F813245A2B1200B896B1 /* XCRemoteSwiftPackageReference "JGProgressHUD" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/JonasGessner/JGProgressHUD";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 2.1.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
CE5B7D022458C921002E5C06 /* Reachability */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = CE5B7D012458C921002E5C06 /* XCRemoteSwiftPackageReference "Reachability" */;
|
||||||
|
productName = Reachability;
|
||||||
|
};
|
||||||
|
CEB269562445DB56004B74B3 /* SQLite */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = CEB269552445DB56004B74B3 /* XCRemoteSwiftPackageReference "SQLite" */;
|
||||||
|
productName = SQLite;
|
||||||
|
};
|
||||||
|
CEC7F814245A2B1200B896B1 /* JGProgressHUD */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = CEC7F813245A2B1200B896B1 /* XCRemoteSwiftPackageReference "JGProgressHUD" */;
|
||||||
|
productName = JGProgressHUD;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = CE56CEC2209D81DD00932C01 /* Project object */;
|
rootObject = CE56CEC2209D81DD00932C01 /* Project object */;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,6 @@
|
|||||||
<Workspace
|
<Workspace
|
||||||
version = "1.0">
|
version = "1.0">
|
||||||
<FileRef
|
<FileRef
|
||||||
location = "self:CapCollector.xcodeproj">
|
location = "self:">
|
||||||
</FileRef>
|
</FileRef>
|
||||||
</Workspace>
|
</Workspace>
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"object": {
|
||||||
|
"pins": [
|
||||||
|
{
|
||||||
|
"package": "JGProgressHUD",
|
||||||
|
"repositoryURL": "https://github.com/JonasGessner/JGProgressHUD",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "08d130dd614a743f813286f096804c43a6ffa3f6",
|
||||||
|
"version": "2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "Reachability",
|
||||||
|
"repositoryURL": "https://github.com/ashleymills/Reachability.swift",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "98e968e7b6c1318fb61df23e347bc319761e8acb",
|
||||||
|
"version": "5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"package": "SQLite.swift",
|
||||||
|
"repositoryURL": "https://github.com/stephencelis/SQLite.swift",
|
||||||
|
"state": {
|
||||||
|
"branch": null,
|
||||||
|
"revision": "0a9893ec030501a3956bee572d6b4fdd3ae158a1",
|
||||||
|
"version": "0.12.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"version": 1
|
||||||
|
}
|
@ -9,6 +9,11 @@
|
|||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>CapCollector.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>0</integer>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
10
CapCollector.xcworkspace/contents.xcworkspacedata
generated
10
CapCollector.xcworkspace/contents.xcworkspacedata
generated
@ -1,10 +0,0 @@
|
|||||||
<?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>
|
|
@ -1,8 +0,0 @@
|
|||||||
<?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>
|
|
@ -1,5 +0,0 @@
|
|||||||
<?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>
|
|
@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Bucket
|
|
||||||
type = "0"
|
|
||||||
version = "2.0">
|
|
||||||
</Bucket>
|
|
@ -8,70 +8,93 @@
|
|||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import CoreData
|
import CoreData
|
||||||
import SwiftyDropbox
|
|
||||||
|
|
||||||
/**
|
import Reachability
|
||||||
TODO:
|
|
||||||
- Mosaic: Prevent swap of tiles when tapping the free space at the right edge
|
#warning("ImageSelector: Allow deletion and moving of an image of a cap")
|
||||||
- Show banner with number of unmatched caps when using camera comparison
|
#warning("ImageSelector: Show icons for failed downloads")
|
||||||
- Feature: Create mosaic from image
|
#warning("GridController: Allow sorting of caps by color")
|
||||||
- Feature: Delete cap
|
#warning("GridController: Reorder caps by dragging")
|
||||||
- Feature: Delete image of cap
|
#warning("GridController: Load and save current mosaic")
|
||||||
*/
|
#warning("GridController: Add option to specify image width")
|
||||||
|
#warning("TableView: Fix blur background of search bar after transition")
|
||||||
|
#warning("TableView: Add banner to jump down to unmatched caps / bottom")
|
||||||
|
|
||||||
var shouldLaunchCamera = false
|
var shouldLaunchCamera = false
|
||||||
|
|
||||||
|
var app: AppDelegate!
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||||
|
|
||||||
enum ShortcutIdentifier: String {
|
// MARK: Static Properties
|
||||||
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
|
/// Main tint color of the app
|
||||||
static let tintColor = UIColor(red: 122/255, green: 155/255, blue: 41/255, alpha: 1)
|
static let tintColor = UIColor(red: 122/255, green: 155/255, blue: 41/255, alpha: 1)
|
||||||
|
|
||||||
var window: UIWindow?
|
var window: UIWindow?
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
var mainStoryboard: UIStoryboard {
|
||||||
|
UIStoryboard(name: "Main", bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
let documentsFolder = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||||
|
|
||||||
|
var database: Database!
|
||||||
|
|
||||||
|
var storage: Storage!
|
||||||
|
|
||||||
|
var reachability: Reachability!
|
||||||
|
|
||||||
|
var needsDownload = false
|
||||||
|
|
||||||
|
var dbUrl: URL {
|
||||||
|
documentsFolder.appendingPathComponent("db.sqlite3")
|
||||||
|
}
|
||||||
|
|
||||||
|
let serverUrl = URL(string: "https://cc.ssl443.org")!
|
||||||
|
|
||||||
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
|
app = self
|
||||||
|
|
||||||
|
storage = Storage(in: documentsFolder)
|
||||||
|
reachability = try! Reachability()
|
||||||
|
|
||||||
|
//resetToFactoryState()
|
||||||
|
|
||||||
|
needsDownload = !FileManager.default.fileExists(atPath: dbUrl.path)
|
||||||
|
database = Database(url: dbUrl, server: serverUrl)
|
||||||
|
|
||||||
|
if needsDownload {
|
||||||
|
log("New database created")
|
||||||
|
} else {
|
||||||
|
let size = (try! FileManager.default.attributesOfItem(atPath: dbUrl.path) as NSDictionary).fileSize()
|
||||||
|
log("Loaded \(database.capCount) caps, database size: \(size) bytes")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
DropboxClientsManager.setupWithAppKey("n81tx2g638wuffl")
|
|
||||||
DiskManager.setupOnFirstLaunch()
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
|
private func resetToFactoryState() {
|
||||||
DropboxController.shared.handle(url: url)
|
for path in try! FileManager.default.contentsOfDirectory(at: documentsFolder, includingPropertiesForKeys: nil) {
|
||||||
return true
|
try! FileManager.default.removeItem(at: path)
|
||||||
|
}
|
||||||
|
UserDefaults.standard.removeObject(forKey: Classifier.userDefaultsKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleShortCutItem(_ shortcutItem: UIApplicationShortcutItem) -> Bool {
|
private func handleShortCutItem(_ shortcutItem: UIApplicationShortcutItem) -> Bool {
|
||||||
event("Shortcut pressed")
|
log("Shortcut pressed")
|
||||||
shouldLaunchCamera = true
|
shouldLaunchCamera = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
func applicationDidBecomeActive(_ application: UIApplication) {
|
||||||
Cap.uploadRemainingImages()
|
app.database?.uploadRemainingImages()
|
||||||
guard shouldLaunchCamera else { return }
|
guard shouldLaunchCamera else { return }
|
||||||
shouldLaunchCamera = false
|
shouldLaunchCamera = false
|
||||||
if let c = (frontmostViewController as? UINavigationController)?.topViewController as? TableView {
|
if let c = (frontmostViewController as? UINavigationController)?.topViewController as? TableView {
|
||||||
c.performSegue(withIdentifier: "showCamera", sender: nil)
|
c.showCameraView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +120,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AppDelegate: Logger {
|
extension AppDelegate: Logger { }
|
||||||
static let logToken = "[AppDelegate]"
|
|
||||||
|
|
||||||
}
|
|
||||||
|
23
CapCollector/Assets.xcassets/mosaic.imageset/Contents.json
vendored
Normal file
23
CapCollector/Assets.xcassets/mosaic.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "picture28.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "picture56.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "picture84.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
CapCollector/Assets.xcassets/mosaic.imageset/picture28.png
vendored
Normal file
BIN
CapCollector/Assets.xcassets/mosaic.imageset/picture28.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
BIN
CapCollector/Assets.xcassets/mosaic.imageset/picture56.png
vendored
Normal file
BIN
CapCollector/Assets.xcassets/mosaic.imageset/picture56.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.8 KiB |
BIN
CapCollector/Assets.xcassets/mosaic.imageset/picture84.png
vendored
Normal file
BIN
CapCollector/Assets.xcassets/mosaic.imageset/picture84.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
23
CapCollector/Assets.xcassets/settings.imageset/Contents.json
vendored
Normal file
23
CapCollector/Assets.xcassets/settings.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "button_settings_white@1x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "button_settings_white@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "button_settings_white@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
CapCollector/Assets.xcassets/settings.imageset/button_settings_white@1x.png
vendored
Normal file
BIN
CapCollector/Assets.xcassets/settings.imageset/button_settings_white@1x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
CapCollector/Assets.xcassets/settings.imageset/button_settings_white@2x.png
vendored
Normal file
BIN
CapCollector/Assets.xcassets/settings.imageset/button_settings_white@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
BIN
CapCollector/Assets.xcassets/settings.imageset/button_settings_white@3x.png
vendored
Normal file
BIN
CapCollector/Assets.xcassets/settings.imageset/button_settings_white@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.0 KiB |
File diff suppressed because it is too large
Load Diff
@ -17,15 +17,15 @@ protocol CameraControllerDelegate {
|
|||||||
|
|
||||||
class CameraController: UIViewController {
|
class CameraController: UIViewController {
|
||||||
|
|
||||||
private let imageSize = 299 // New for XCode models, 227/229 for turicreate
|
|
||||||
|
|
||||||
// MARK: Outlets
|
// MARK: Outlets
|
||||||
|
|
||||||
@IBOutlet weak var imageButton: RoundedButton!
|
@IBOutlet weak var imageButton: UIButton!
|
||||||
|
|
||||||
@IBOutlet weak var cropView: CropView!
|
@IBOutlet weak var cropView: CropView!
|
||||||
|
|
||||||
@IBOutlet weak var cancelButton: RoundedButton!
|
@IBOutlet weak var cancelButton: UIButton!
|
||||||
|
|
||||||
|
@IBOutlet weak var bottomBar: UIView!
|
||||||
|
|
||||||
@IBOutlet weak var cameraView: CameraView! {
|
@IBOutlet weak var cameraView: CameraView! {
|
||||||
didSet {
|
didSet {
|
||||||
@ -89,12 +89,24 @@ class CameraController: UIViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func setTintColor() {
|
private func setTintColor() {
|
||||||
let tint = AppDelegate.tintColor
|
let blur = UIBlurEffect(style: .systemThinMaterial)
|
||||||
cropView.lineColor = tint
|
let a = UIVisualEffectView(effect: blur)
|
||||||
imageButton.borderColor = tint
|
a.translatesAutoresizingMaskIntoConstraints = false
|
||||||
imageButton.imageView?.tintColor = tint
|
bottomBar.backgroundColor = nil
|
||||||
cancelButton.borderColor = tint
|
bottomBar.insertSubview(a, at: 0)
|
||||||
cancelButton.set(template: "cancel", with: tint)
|
//bottomBar.addSubview(a)
|
||||||
|
let t = bottomBar.topAnchor.constraint(equalTo: a.topAnchor)
|
||||||
|
let b = bottomBar.bottomAnchor.constraint(equalTo: a.bottomAnchor)
|
||||||
|
let l = bottomBar.leadingAnchor.constraint(equalTo: a.leadingAnchor)
|
||||||
|
let r = bottomBar.trailingAnchor.constraint(equalTo: a.trailingAnchor)
|
||||||
|
bottomBar.addConstraints([t,b,l,r])
|
||||||
|
//let tint = AppDelegate.tintColor
|
||||||
|
//cropView.lineColor = tint
|
||||||
|
//imageButton.borderColor = tint
|
||||||
|
imageButton.imageView?.tintColor = .systemBlue
|
||||||
|
|
||||||
|
//cancelButton.borderColor = tint
|
||||||
|
//cancelButton.set(template: "cancel", with: tint)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func giveFeedback(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
|
private func giveFeedback(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
|
||||||
@ -107,8 +119,8 @@ class CameraController: UIViewController {
|
|||||||
private func showNoCameraAccessAlert() {
|
private func showNoCameraAccessAlert() {
|
||||||
let alert = UIAlertController(title: "Unable to access the Camera",
|
let alert = UIAlertController(title: "Unable to access the Camera",
|
||||||
message: "To enable access, go to Settings > Privacy > Camera and turn on Camera access for this app.",
|
message: "To enable access, go to Settings > Privacy > Camera and turn on Camera access for this app.",
|
||||||
preferredStyle: .alert,
|
preferredStyle: .alert)//,
|
||||||
blurStyle: .dark)
|
//blurStyle: .dark)
|
||||||
|
|
||||||
let okAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
|
let okAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
|
||||||
alert.addAction(okAction)
|
alert.addAction(okAction)
|
||||||
@ -132,10 +144,9 @@ class CameraController: UIViewController {
|
|||||||
extension CameraController: PhotoCaptureHandlerDelegate {
|
extension CameraController: PhotoCaptureHandlerDelegate {
|
||||||
|
|
||||||
func didCapture(_ image: UIImage?) {
|
func didCapture(_ image: UIImage?) {
|
||||||
event("Image captured")
|
log("Image captured")
|
||||||
let factor = CGFloat(cropView.relativeSize)
|
let factor = CGFloat(cropView.relativeSize)
|
||||||
self.dismiss(animated: true)
|
self.dismiss(animated: true)
|
||||||
let size = CGSize(width: imageSize, height: imageSize)
|
|
||||||
guard let img = image else {
|
guard let img = image else {
|
||||||
self.error("No image captured")
|
self.error("No image captured")
|
||||||
return
|
return
|
||||||
@ -151,14 +162,10 @@ extension CameraController: PhotoCaptureHandlerDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only use 227 x 227 image
|
let scaled = masked.resize(to: Cap.imageSize)
|
||||||
let scaled = masked.resize(to: size)
|
|
||||||
|
|
||||||
delegate!.didCapture(image: scaled)
|
delegate!.didCapture(image: scaled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CameraController: Logger {
|
extension CameraController: Logger { }
|
||||||
|
|
||||||
static var logToken = "[Camera]"
|
|
||||||
}
|
|
||||||
|
@ -191,7 +191,7 @@ class CameraView: UIView {
|
|||||||
|
|
||||||
photoOutput.isHighResolutionCaptureEnabled = true
|
photoOutput.isHighResolutionCaptureEnabled = true
|
||||||
photoOutput.isDepthDataDeliveryEnabled = false
|
photoOutput.isDepthDataDeliveryEnabled = false
|
||||||
photoOutput.isDualCameraDualPhotoDeliveryEnabled = false
|
//photoOutput.isDualCameraDualPhotoDeliveryEnabled = false
|
||||||
photoOutput.isLivePhotoCaptureEnabled = false
|
photoOutput.isLivePhotoCaptureEnabled = false
|
||||||
session.commitConfiguration()
|
session.commitConfiguration()
|
||||||
}
|
}
|
||||||
@ -200,7 +200,7 @@ class CameraView: UIView {
|
|||||||
let screenSize = bounds.size
|
let screenSize = bounds.size
|
||||||
let location = touch.location(in: self)
|
let location = touch.location(in: self)
|
||||||
let focusPoint = CGPoint(x: location.y / screenSize.height, y: 1.0 - location.x / screenSize.width)
|
let focusPoint = CGPoint(x: location.y / screenSize.height, y: 1.0 - location.x / screenSize.width)
|
||||||
event("Focusing on point (\(focusPoint.x),\(focusPoint.y))")
|
log("Focusing on point (\(focusPoint.x),\(focusPoint.y))")
|
||||||
if let device = cameraDevice {
|
if let device = cameraDevice {
|
||||||
do {
|
do {
|
||||||
try device.lockForConfiguration()
|
try device.lockForConfiguration()
|
||||||
@ -221,7 +221,4 @@ class CameraView: UIView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension CameraView: Logger {
|
extension CameraView: Logger { }
|
||||||
|
|
||||||
static let logToken = "CameraView"
|
|
||||||
}
|
|
||||||
|
@ -7,6 +7,7 @@ Photo capture delegate.
|
|||||||
|
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import Photos
|
import Photos
|
||||||
|
import UIKit
|
||||||
|
|
||||||
protocol PhotoCaptureHandlerDelegate {
|
protocol PhotoCaptureHandlerDelegate {
|
||||||
|
|
||||||
@ -32,13 +33,13 @@ extension PhotoCaptureHandler: AVCapturePhotoCaptureDelegate {
|
|||||||
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
|
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
|
||||||
|
|
||||||
guard error == nil else {
|
guard error == nil else {
|
||||||
print("PhotoCaptureHandler: \(error!)")
|
log("PhotoCaptureHandler: \(error!)")
|
||||||
delegate?.didCapture(nil)
|
delegate?.didCapture(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let cgImage = photo.cgImageRepresentation()?.takeUnretainedValue() else {
|
guard let cgImage = photo.cgImageRepresentation()?.takeUnretainedValue() else {
|
||||||
print("PhotoCaptureHandler: No image captured")
|
log("PhotoCaptureHandler: No image captured")
|
||||||
delegate?.didCapture(nil)
|
delegate?.didCapture(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -49,3 +50,7 @@ extension PhotoCaptureHandler: AVCapturePhotoCaptureDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension PhotoCaptureHandler: Logger {
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -1,180 +0,0 @@
|
|||||||
//
|
|
||||||
// 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 notify = false
|
|
||||||
|
|
||||||
private var image: UIImage?
|
|
||||||
|
|
||||||
/**
|
|
||||||
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) {
|
|
||||||
self.image = image
|
|
||||||
notify = reportingImage
|
|
||||||
if Persistence.useMobileNet {
|
|
||||||
performClassifications(model: MobileNet().model)
|
|
||||||
} else {
|
|
||||||
performClassifications(model: Squeezenet().model)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func performClassifications(model: MLModel) {
|
|
||||||
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 model = try! VNCoreMLModel(for: model)
|
|
||||||
|
|
||||||
let request = VNCoreMLRequest(model: model, completionHandler: { [weak self] request, error in
|
|
||||||
guard self != nil else {
|
|
||||||
Classifier.event("Self not captured, instance deallocated?")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self!.process(request: request, error: error)
|
|
||||||
})
|
|
||||||
request.imageCropAndScaleOption = .centerCrop
|
|
||||||
do {
|
|
||||||
try handler.perform([request])
|
|
||||||
} catch {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.report(error: "Classification failed: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func process(request: VNRequest, error: Error?) {
|
|
||||||
if let e = error {
|
|
||||||
report(error: "Unable to classify image: \(e.localizedDescription)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if let result = request.results as? [VNClassificationObservation] {
|
|
||||||
let classification = dict(from: result)
|
|
||||||
process(classification: classification)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if let result = (request.results as? [VNCoreMLFeatureValueObservation])?.first?.featureValue.multiArrayValue {
|
|
||||||
let classification = dict(from: result)
|
|
||||||
process(classification: classification)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
report(error: "Invalid classifier result: \(String(describing: request.results))")
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private func process(classification: [Int : Float]) {
|
|
||||||
Cap.unsortedCaps.forEach { cap in
|
|
||||||
cap.match = classification[cap.id] ?? 0
|
|
||||||
}
|
|
||||||
Cap.hasMatches = true
|
|
||||||
|
|
||||||
// Update the count of recognized counts
|
|
||||||
Persistence.recognizedCapCount = classification.count
|
|
||||||
|
|
||||||
report()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func dict(from results: MLMultiArray) -> [Int : Float] {
|
|
||||||
let length = results.count
|
|
||||||
let doublePtr = results.dataPointer.bindMemory(to: Double.self, capacity: length)
|
|
||||||
let doubleBuffer = UnsafeBufferPointer(start: doublePtr, count: length)
|
|
||||||
let output = Array(doubleBuffer).enumerated().map {
|
|
||||||
($0.offset + 1, Float($0.element))
|
|
||||||
}
|
|
||||||
return [Int : Float](uniqueKeysWithValues: output)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Callbacks
|
|
||||||
|
|
||||||
private func report(error message: String) {
|
|
||||||
guard delegate != nil else {
|
|
||||||
error("No delegate: " + message)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.image = nil
|
|
||||||
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.image = nil
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -10,19 +10,18 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
import CoreImage
|
import CoreImage
|
||||||
|
|
||||||
import SwiftyDropbox
|
import SQLite
|
||||||
|
|
||||||
protocol CapsDelegate: class {
|
struct Cap {
|
||||||
|
|
||||||
func capHasUpdates(_ cap: Cap)
|
|
||||||
|
|
||||||
func capsLoaded()
|
|
||||||
}
|
|
||||||
|
|
||||||
final class Cap {
|
|
||||||
|
|
||||||
// MARK: - Static variables
|
// MARK: - Static variables
|
||||||
|
|
||||||
|
static let sufficientImageCount = 10
|
||||||
|
|
||||||
|
static let imageWidth = 299 // New for XCode models, 227/229 for turicreate
|
||||||
|
|
||||||
|
static let imageSize = CGSize(width: imageWidth, height: imageWidth)
|
||||||
|
|
||||||
static let jpgQuality: CGFloat = 0.3
|
static let jpgQuality: CGFloat = 0.3
|
||||||
|
|
||||||
private static let mosaicColumns = 40
|
private static let mosaicColumns = 40
|
||||||
@ -33,657 +32,202 @@ final class Cap {
|
|||||||
|
|
||||||
private static let mosaicMargin = mosaicCellSize - mosaicRowHeight
|
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
|
// MARK: - Variables
|
||||||
|
|
||||||
/// The unique number of the cap
|
/// The unique number of the cap
|
||||||
let id: Int
|
let id: Int
|
||||||
|
|
||||||
/// The tile position of the cap
|
/// The tile position of the cap
|
||||||
var tile: Int
|
let tile: Int
|
||||||
|
|
||||||
/// The name of the cap
|
/// The name of the cap
|
||||||
var name: String {
|
let 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
|
/// The name of the cap without special characters
|
||||||
private(set) var cleanName: String
|
let cleanName: String
|
||||||
|
|
||||||
/// The number of images existing for the cap
|
/// The number of images existing for the cap
|
||||||
private(set) var count: Int {
|
let count: Int
|
||||||
didSet {
|
|
||||||
Cap.save()
|
|
||||||
event("Updated count for cap \(id) to \(count)")
|
|
||||||
Cap.delegate?.capHasUpdates(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The average color of the cap
|
/// The average color of the cap
|
||||||
var color: UIColor?
|
let color: UIColor
|
||||||
|
|
||||||
/// The similarity of the cap to the currently processed image
|
/// Indicate if the cap can be found by the recognition model
|
||||||
var match: Float? = nil
|
let matched: Bool
|
||||||
|
|
||||||
// MARK: - All caps
|
/// Indicate if the cap is present on the server
|
||||||
|
let uploaded: Bool
|
||||||
|
|
||||||
/// A dictionary of all known caps
|
// MARK: Init
|
||||||
static var all = [Int : Cap]()
|
|
||||||
|
|
||||||
// MARK: - Tile information
|
init(name: String, id: Int, color: UIColor) {
|
||||||
|
self.id = id
|
||||||
/// A dictionary of the caps for the tiles
|
self.count = 1
|
||||||
static var tiles = [Int : Cap]()
|
|
||||||
|
|
||||||
/**
|
|
||||||
Get the cap image for a tile.
|
|
||||||
*/
|
|
||||||
static func tileImage(tile: Int) -> UIImage? {
|
|
||||||
return tiles[tile]?.thumbnail
|
|
||||||
}
|
|
||||||
|
|
||||||
static func tileColor(tile: Int) -> UIColor? {
|
|
||||||
return tiles[tile]?.averageColor
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.name = name
|
||||||
self.count = 0
|
self.cleanName = ""
|
||||||
|
self.tile = id
|
||||||
|
self.color = color
|
||||||
|
self.matched = false
|
||||||
|
self.uploaded = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: SQLite
|
||||||
|
|
||||||
|
static let table = Table("data")
|
||||||
|
|
||||||
|
static let createQuery: String = {
|
||||||
|
table.create(ifNotExists: true) { t in
|
||||||
|
t.column(rowId, primaryKey: true)
|
||||||
|
t.column(rowName)
|
||||||
|
t.column(rowCount)
|
||||||
|
t.column(rowTile)
|
||||||
|
t.column(rowRed)
|
||||||
|
t.column(rowGreen)
|
||||||
|
t.column(rowBlue)
|
||||||
|
t.column(rowMatched)
|
||||||
|
t.column(rowUploaded)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
static let rowId = Expression<Int>("id")
|
||||||
|
|
||||||
|
static let rowName = Expression<String>("name")
|
||||||
|
|
||||||
|
static let rowCount = Expression<Int>("count")
|
||||||
|
|
||||||
|
static let rowTile = Expression<Int>("tile")
|
||||||
|
|
||||||
|
static let rowRed = Expression<Int>("red")
|
||||||
|
static let rowGreen = Expression<Int>("green")
|
||||||
|
static let rowBlue = Expression<Int>("blue")
|
||||||
|
|
||||||
|
static let rowMatched = Expression<Bool>("matched")
|
||||||
|
|
||||||
|
static let rowUploaded = Expression<Bool>("uploaded")
|
||||||
|
|
||||||
|
init(row: Row) {
|
||||||
|
self.id = row[Cap.rowId]
|
||||||
|
self.name = row[Cap.rowName]
|
||||||
|
self.count = row[Cap.rowCount]
|
||||||
|
self.tile = row[Cap.rowTile]
|
||||||
self.cleanName = name.clean
|
self.cleanName = name.clean
|
||||||
guard save(mainImage: image) else {
|
self.matched = row[Cap.rowMatched]
|
||||||
return nil
|
self.uploaded = row[Cap.rowUploaded]
|
||||||
}
|
|
||||||
Cap.all[self.id] = self
|
|
||||||
Cap.tiles[self.id] = self
|
|
||||||
Cap.shouldCreateFolderForCap(self.id)
|
|
||||||
Cap.save()
|
|
||||||
Cap.delegate?.capHasUpdates(self)
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
|
||||||
self.add(image: image) { _ in
|
|
||||||
|
|
||||||
}
|
let r = CGFloat(row[Cap.rowRed]) / 255
|
||||||
}
|
let g = CGFloat(row[Cap.rowGreen]) / 255
|
||||||
|
let b = CGFloat(row[Cap.rowBlue]) / 255
|
||||||
|
self.color = UIColor(red: r, green: g, blue: b, alpha: 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
init(id: Int, name: String, count: Int) {
|
||||||
Create a cap from a line in the cap list file
|
self.id = id
|
||||||
*/
|
self.name = name
|
||||||
init?(line: String) {
|
|
||||||
guard line != "" else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let parts = line.components(separatedBy: ";")
|
|
||||||
guard parts.count == 4 || parts.count == 8 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
|
|
||||||
}
|
|
||||||
if parts.count == 8 {
|
|
||||||
guard let r = Int(parts[4]), let g = Int(parts[5]), let b = Int(parts[6]), let a = Int(parts[7]) else {
|
|
||||||
Cap.error("Invalid color in line \(line)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
self.color = UIColor(red: CGFloat(r)/255, green: CGFloat(g)/255, blue: CGFloat(b)/255, alpha: CGFloat(a)/255)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.id = nr
|
|
||||||
self.name = parts[1]
|
|
||||||
self.count = count
|
self.count = count
|
||||||
|
self.tile = id - 1
|
||||||
self.cleanName = name.clean
|
self.cleanName = name.clean
|
||||||
self.tile = tile
|
self.matched = false
|
||||||
Cap.tiles[tile] = self
|
self.color = UIColor.gray
|
||||||
Cap.all[id] = self
|
self.uploaded = false
|
||||||
|
}
|
||||||
|
|
||||||
|
var insertQuery: Insert {
|
||||||
|
let colors = color.rgb
|
||||||
|
return Cap.table.insert(
|
||||||
|
Cap.rowId <- id,
|
||||||
|
Cap.rowName <- name,
|
||||||
|
Cap.rowCount <- count,
|
||||||
|
Cap.rowTile <- tile,
|
||||||
|
Cap.rowRed <- colors.red,
|
||||||
|
Cap.rowGreen <- colors.green,
|
||||||
|
Cap.rowBlue <- colors.blue,
|
||||||
|
Cap.rowMatched <- matched,
|
||||||
|
Cap.rowUploaded <- uploaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Text
|
||||||
|
|
||||||
|
func matchDescription(match: Float?) -> String {
|
||||||
|
guard let match = match else {
|
||||||
|
return hasSufficientImages ? "" : "⚠️"
|
||||||
|
}
|
||||||
|
let percent = Int((match * 100).rounded())
|
||||||
|
return String(format: "%d %%", arguments: [percent])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cap id and the number of images
|
||||||
|
var subtitle: String {
|
||||||
|
guard count != 1 else {
|
||||||
|
return "\(id) (1 image)"
|
||||||
|
}
|
||||||
|
return "\(id) (\(count) images)"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Images
|
// MARK: - Images
|
||||||
|
|
||||||
|
var hasSufficientImages: Bool {
|
||||||
|
count > Cap.sufficientImageCount
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasImage: Bool {
|
||||||
|
app.storage.hasImage(for: id)
|
||||||
|
}
|
||||||
|
|
||||||
/// The main image of the cap
|
/// The main image of the cap
|
||||||
var image: UIImage? {
|
var image: UIImage? {
|
||||||
guard let data = DiskManager.image(for: id) else {
|
app.storage.image(for: id)
|
||||||
self.downloadImage { _ in
|
|
||||||
Cap.delegate?.capHasUpdates(self)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return UIImage(data: data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The main image of the cap
|
/// The main image of the cap
|
||||||
var thumbnail: UIImage? {
|
var thumbnail: UIImage? {
|
||||||
if let data = DiskManager.thumbnail(for: id) {
|
app.storage.thumbnail(for: id)
|
||||||
return UIImage(data: data)
|
|
||||||
}
|
|
||||||
return makeThumbnail()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var averageColor: UIColor? {
|
static func thumbnail(for image: UIImage) -> UIImage {
|
||||||
if let c = color {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
return makeAverageColor()
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func makeThumbnail() -> UIImage? {
|
|
||||||
guard let img = image else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let len = GridViewController.len * 2
|
let len = GridViewController.len * 2
|
||||||
let thumb = img.resize(to: CGSize.init(width: len, height: len))
|
return image.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)")
|
func updateLocalThumbnail() {
|
||||||
return thumb
|
guard let img = image else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let thumbnail = Cap.thumbnail(for: img)
|
||||||
|
guard app.storage.save(thumbnail: thumbnail, for: id) else {
|
||||||
|
error("Failed to save thumbnail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log("Created thumbnail for cap \(id)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateLocalColor() {
|
||||||
|
guard let color = image?.averageColor else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.database.update(color: color, for: id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Download the main image of the cap.
|
||||||
|
- Note: The downloaded image is automatically saved to disk
|
||||||
|
- returns: `true`, if the image will be downloaded, `false`, if the image is already being downloaded.
|
||||||
|
*/
|
||||||
|
@discardableResult
|
||||||
|
func downloadMainImage(completion: @escaping (_ image: UIImage?) -> Void) -> Bool {
|
||||||
|
app.database.downloadMainImage(for: id, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Download a specified image of the cap.
|
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 number: The number of the image
|
||||||
- parameter completion: The completion handler, called with the image if successful
|
- parameter completion: The completion handler, called with the image if successful
|
||||||
- parameter image: The image, if the download was successful, or nil on error
|
- parameter image: The image, if the download was successful, or nil on error
|
||||||
|
- returns: `true`, if the image will be downloaded, `false`, if the image is already being downloaded.
|
||||||
*/
|
*/
|
||||||
func downloadImage(_ number: Int = 0, completion: @escaping (_ image: UIImage?) -> Void) {
|
func downloadImage(_ number: Int, completion: @escaping (_ image: UIImage?) -> Void) -> Bool {
|
||||||
let path = imageFolderPath + "/\(id)-\(number).jpg"
|
app.database.downloadImage(for: id, version: number, completion: completion)
|
||||||
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 main image to data for cap \(id)")
|
|
||||||
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")
|
|
||||||
makeThumbnail()
|
|
||||||
|
|
||||||
Cap.delegate?.capHasUpdates(self)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func add(image: UIImage, completion: @escaping (Bool) -> Void) {
|
|
||||||
self.upload(image: image) { saved in
|
|
||||||
guard saved else {
|
|
||||||
completion(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Increment cap count
|
|
||||||
self.count += 1
|
|
||||||
completion(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var imageFolderPath: String {
|
|
||||||
return String(format: "/Images/%04d", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func imageFolderPath(for cap: Int) -> String {
|
|
||||||
return String(format: "/Images/%04d", cap)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func imageFilePath(imageId: Int) -> String {
|
|
||||||
return imageFolderPath + "/\(id)-\(imageId).jpg"
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Image upload
|
|
||||||
|
|
||||||
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 static func createFolder(forCap cap: Int, completion: @escaping (_ success: Bool) -> Void) {
|
|
||||||
guard shouldCreateFolder(forCap: cap) else {
|
|
||||||
completion(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Create folder for cap
|
|
||||||
let path = imageFolderPath(for: cap)
|
|
||||||
DropboxController.client.files.createFolderV2(path: path).response { _, error in
|
|
||||||
if let err = error {
|
|
||||||
self.event("Could not create folder for cap \(cap): \(err)")
|
|
||||||
completion(false)
|
|
||||||
} else {
|
|
||||||
self.event("Created folder for cap \(cap)")
|
|
||||||
didCreateFolder(forCap: cap)
|
|
||||||
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.uploadCapImage(at: url, forCap: id) { success in
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func uploadCapImage(at url: URL, forCap cap: Int, completion: @escaping (Bool) -> Void) {
|
|
||||||
createFolder(forCap: cap) { created in
|
|
||||||
guard created else { return }
|
|
||||||
uploadCapImage(at: url, forCapWithExistingFolder: cap, completion: completion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func uploadCapImage(at url: URL, forCapWithExistingFolder cap: Int, completion: @escaping (Bool) -> Void) {
|
|
||||||
let path = imageFolderPath(for: 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 {
|
|
||||||
let cap = Int(url.lastPathComponent.components(separatedBy: "-").first!)!
|
|
||||||
uploadCapImage(at: url, forCap: cap) { 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setMainImage(to imageId: Int, image: UIImage) {
|
|
||||||
guard imageId != 0 else {
|
|
||||||
self.event("No need to switch main image with itself")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let tempFile = imageFilePath(imageId: count)
|
|
||||||
let oldFile = imageFilePath(imageId: 0)
|
|
||||||
let newFile = imageFilePath(imageId: imageId)
|
|
||||||
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)")
|
|
||||||
}
|
|
||||||
guard self.save(mainImage: image) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.event("Successfully set image \(imageId) to main image for cap \(self.id)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
let path = imageFolderPath
|
|
||||||
DropboxController.client.files.listFolder(path: path).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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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: - Folders to upload
|
|
||||||
|
|
||||||
static func shouldCreateFolderForCap(_ cap: Int) {
|
|
||||||
let oldCaps = Persistence.folderNotCreated
|
|
||||||
guard !oldCaps.contains(cap) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let newCaps = oldCaps + [cap]
|
|
||||||
Persistence.folderNotCreated = newCaps
|
|
||||||
}
|
|
||||||
|
|
||||||
static func shouldCreateFolder(forCap cap: Int) -> Bool {
|
|
||||||
return Persistence.folderNotCreated.contains(cap)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func didCreateFolder(forCap cap: Int) {
|
|
||||||
let oldCaps = Persistence.folderNotCreated
|
|
||||||
guard oldCaps.contains(cap) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Persistence.folderNotCreated = oldCaps.filter { $0 != cap }
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Average color
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func makeAverageColor() -> UIColor? {
|
|
||||||
guard let url = DiskManager.imageUrlForCap(id) else {
|
|
||||||
event("No main image for cap \(id), no average color calculated")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let inputImage = CIImage(contentsOf: url) else {
|
|
||||||
error("Failed to read CIImage for main image of cap \(id)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let extentVector = CIVector(x: inputImage.extent.origin.x, y: inputImage.extent.origin.y, z: inputImage.extent.size.width, w: inputImage.extent.size.height)
|
|
||||||
|
|
||||||
guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: inputImage, kCIInputExtentKey: extentVector]) else {
|
|
||||||
error("Failed to create filter to calculate average for cap \(id)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let outputImage = filter.outputImage else {
|
|
||||||
error("Failed get filter output for image of cap \(id)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var bitmap = [UInt8](repeating: 0, count: 4)
|
|
||||||
let context = CIContext(options: [.workingColorSpace: kCFNull])
|
|
||||||
context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil)
|
|
||||||
|
|
||||||
color = UIColor(
|
|
||||||
red: saturate(bitmap[0]),
|
|
||||||
green: saturate(bitmap[1]),
|
|
||||||
blue: saturate(bitmap[2]),
|
|
||||||
alpha: CGFloat(bitmap[3]) / 255)
|
|
||||||
|
|
||||||
event("Average color updated for cap \(id)")
|
|
||||||
Cap.save()
|
|
||||||
return color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Map expected range 75-200 to 0-255
|
|
||||||
private func saturate(_ component: UInt8) -> CGFloat {
|
|
||||||
return max(min(CGFloat(component) * 2 - 150, 255), 0) / 255
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Protocol Hashable
|
// MARK: - Protocol Hashable
|
||||||
@ -705,31 +249,14 @@ extension Cap: Hashable {
|
|||||||
extension Cap: CustomStringConvertible {
|
extension Cap: CustomStringConvertible {
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
guard let c = color else {
|
let rgb = color.rgb
|
||||||
return String(format: "%04d", id) + ";\(name);\(count);\(tile)\n"
|
return String(format: "%04d", id) + ";\(name);\(count);\(tile);\(rgb.red);\(rgb.green);\(rgb.blue)\n"
|
||||||
}
|
|
||||||
|
|
||||||
var fRed: CGFloat = 0
|
|
||||||
var fGreen: CGFloat = 0
|
|
||||||
var fBlue: CGFloat = 0
|
|
||||||
var fAlpha: CGFloat = 0
|
|
||||||
guard c.getRed(&fRed, green: &fGreen, blue: &fBlue, alpha: &fAlpha) else {
|
|
||||||
return String(format: "%04d", id) + ";\(name);\(count);\(tile)\n"
|
|
||||||
}
|
|
||||||
let r = Int(fRed * 255.0)
|
|
||||||
let g = Int(fGreen * 255.0)
|
|
||||||
let b = Int(fBlue * 255.0)
|
|
||||||
let a = Int(fAlpha * 255.0)
|
|
||||||
return String(format: "%04d", id) + ";\(name);\(count);\(tile);\(r);\(g);\(b);\(a)\n"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Protocol Logger
|
// MARK: - Protocol Logger
|
||||||
|
|
||||||
extension Cap: Logger {
|
extension Cap: Logger { }
|
||||||
|
|
||||||
static let logToken = "[CAP]"
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - String extension
|
// MARK: - String extension
|
||||||
|
|
||||||
|
68
CapCollector/Data/Classifier.swift
Normal file
68
CapCollector/Data/Classifier.swift
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
//
|
||||||
|
// VisionHandler.swift
|
||||||
|
// CapFinder
|
||||||
|
//
|
||||||
|
// Created by User on 12.02.18.
|
||||||
|
// Copyright © 2018 User. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Vision
|
||||||
|
import CoreML
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// Recognise categories in images
|
||||||
|
class Classifier: Logger {
|
||||||
|
|
||||||
|
static let userDefaultsKey = "classifier.version"
|
||||||
|
|
||||||
|
let model: VNCoreMLModel
|
||||||
|
|
||||||
|
init(model: VNCoreMLModel) {
|
||||||
|
self.model = model
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
Classify an image
|
||||||
|
- Parameter image: The image to classify
|
||||||
|
- Note: This method should not be scheduled on the main thread.
|
||||||
|
*/
|
||||||
|
func recognize(image: UIImage, completion: @escaping (_ matches: [Int: Float]?) -> Void) {
|
||||||
|
guard let ciImage = CIImage(image: image) else {
|
||||||
|
error("Unable to create CIImage")
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let orientation = CGImagePropertyOrientation(image.imageOrientation)
|
||||||
|
let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
|
||||||
|
let request = VNCoreMLRequest(model: model) { request, error in
|
||||||
|
let matches = self.process(request: request, error: error)
|
||||||
|
completion(matches)
|
||||||
|
}
|
||||||
|
request.imageCropAndScaleOption = .centerCrop
|
||||||
|
do {
|
||||||
|
try handler.perform([request])
|
||||||
|
} catch {
|
||||||
|
self.error("Failed to perform classification: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func process(request: VNRequest, error: Error?) -> [Int : Float]? {
|
||||||
|
if let e = error {
|
||||||
|
self.error("Unable to classify image: \(e.localizedDescription)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let result = request.results as? [VNClassificationObservation] else {
|
||||||
|
self.error("Invalid classifier result: \(String(describing: request.results))")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let matches = result.reduce(into: [:]) { $0[Int($1.identifier)!] = $1.confidence }
|
||||||
|
|
||||||
|
log("Classifed image with \(matches.count) classes")
|
||||||
|
DispatchQueue.global(qos: .background).async {
|
||||||
|
app.database.update(recognizedCaps: Set(matches.keys))
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
}
|
621
CapCollector/Data/Database.swift
Normal file
621
CapCollector/Data/Database.swift
Normal file
@ -0,0 +1,621 @@
|
|||||||
|
//
|
||||||
|
// Database.swift
|
||||||
|
// CapCollector
|
||||||
|
//
|
||||||
|
// Created by Christoph on 14.04.20.
|
||||||
|
// Copyright © 2020 CH. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import CoreML
|
||||||
|
import SQLite
|
||||||
|
|
||||||
|
protocol DatabaseDelegate: class {
|
||||||
|
|
||||||
|
func database(didChangeCap cap: Int)
|
||||||
|
|
||||||
|
func database(didAddCap cap: Cap)
|
||||||
|
|
||||||
|
func databaseRequiresFullRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Weak {
|
||||||
|
|
||||||
|
weak var value : DatabaseDelegate?
|
||||||
|
|
||||||
|
init (_ value: DatabaseDelegate) {
|
||||||
|
self.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Array where Element == Weak {
|
||||||
|
|
||||||
|
mutating func reap () {
|
||||||
|
self = self.filter { $0.value != nil }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Database {
|
||||||
|
|
||||||
|
// MARK: Variables
|
||||||
|
|
||||||
|
let db: Connection
|
||||||
|
|
||||||
|
let upload: Upload
|
||||||
|
|
||||||
|
let download: Download
|
||||||
|
|
||||||
|
private var listeners = [Weak]()
|
||||||
|
|
||||||
|
// MARK: Listeners
|
||||||
|
|
||||||
|
func add(listener: DatabaseDelegate) {
|
||||||
|
listeners.append(Weak(listener))
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(url: URL, server: URL) {
|
||||||
|
guard let db = try? Connection(url.path) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let upload = Upload(server: server)
|
||||||
|
let download = Download(server: server)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try db.run(Cap.createQuery)
|
||||||
|
try db.run(upload.createQuery)
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.db = db
|
||||||
|
self.upload = upload
|
||||||
|
self.download = download
|
||||||
|
log("Database loaded with \(capCount) caps")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Computed properties
|
||||||
|
|
||||||
|
/// All caps currently in the database
|
||||||
|
var caps: [Cap] {
|
||||||
|
(try? db.prepare(Cap.table))?.map(Cap.init) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The ids of the caps which weren't included in the last classification
|
||||||
|
var unmatchedCaps: [Int] {
|
||||||
|
let query = Cap.table.select(Cap.rowId).filter(Cap.rowMatched == false)
|
||||||
|
return (try? db.prepare(query).map { $0[Cap.rowId] }) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of caps which could be recognized during the last classification
|
||||||
|
var recognizedCapCount: Int {
|
||||||
|
(try? db.scalar(Cap.table.filter(Cap.rowMatched == true).count)) ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of caps currently in the database
|
||||||
|
var capCount: Int {
|
||||||
|
(try? db.scalar(Cap.table.count)) ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The total number of images for all caps
|
||||||
|
var imageCount: Int {
|
||||||
|
(try? db.prepare(Cap.table).reduce(0) { $0 + $1[Cap.rowCount] }) ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of caps without a downloaded image
|
||||||
|
var capsWithoutImages: Int {
|
||||||
|
caps.filter({ !$0.hasImage }).count
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var pendingUploads: [(cap: Int, version: Int)] {
|
||||||
|
do {
|
||||||
|
return try db.prepare(upload.table).map { row in
|
||||||
|
(cap: row[upload.rowCapId], version: row[upload.rowCapVersion])
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
log("Failed to get pending uploads")
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indicate if there are any unfinished uploads
|
||||||
|
var hasPendingUploads: Bool {
|
||||||
|
((try? db.scalar(upload.table.count)) ?? 0) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var classifierVersion: Int {
|
||||||
|
set {
|
||||||
|
UserDefaults.standard.set(newValue, forKey: Classifier.userDefaultsKey)
|
||||||
|
log("Classifier version set to \(newValue)")
|
||||||
|
}
|
||||||
|
get {
|
||||||
|
UserDefaults.standard.integer(forKey: Classifier.userDefaultsKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Data updates
|
||||||
|
|
||||||
|
/**
|
||||||
|
Create a new cap with an image.
|
||||||
|
|
||||||
|
The cap is inserted into the database, and the name and image will be uploaded to the server.
|
||||||
|
|
||||||
|
- parameter image: The main image of the cap
|
||||||
|
- parameter name: The name of the cap
|
||||||
|
- note: Must be called on the main queue.
|
||||||
|
- note: The registered delegates will be informed about the added cap through `database(didAddCap:)`
|
||||||
|
- returns: `true`, if the cap was created.
|
||||||
|
*/
|
||||||
|
func createCap(image: UIImage, name: String) -> Bool {
|
||||||
|
guard let color = image.averageColor else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let cap = Cap(name: name, id: capCount, color: color)
|
||||||
|
guard insert(cap: cap) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard app.storage.save(image: image, for: cap.id) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
listeners.forEach { $0.value?.database(didAddCap: cap) }
|
||||||
|
upload.upload(name: name, for: cap.id) { success in
|
||||||
|
guard success else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.update(uploaded: true, for: cap.id)
|
||||||
|
self.upload.uploadImage(for: cap.id, version: 0) { count in
|
||||||
|
guard let count = count else {
|
||||||
|
self.log("Failed to upload first image for cap \(cap.id)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.log("Uploaded first image for cap \(cap.id)")
|
||||||
|
self.update(count: count, for: cap.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Insert a new cap.
|
||||||
|
|
||||||
|
Only inserts the cap into the database, and optionally notifies the delegates.
|
||||||
|
- note: When a new cap is created, use `createCap(image:name:)` instead
|
||||||
|
*/
|
||||||
|
@discardableResult
|
||||||
|
private func insert(cap: Cap, notifyDelegate: Bool = true) -> Bool {
|
||||||
|
do {
|
||||||
|
try db.run(cap.insertQuery)
|
||||||
|
if notifyDelegate {
|
||||||
|
listeners.forEach { $0.value?.database(didAddCap: cap) }
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
log("Failed to insert cap \(cap.id): \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func add(image: UIImage, for cap: Int) -> Bool {
|
||||||
|
guard let version = count(for: cap) else {
|
||||||
|
log("Failed to get count for cap \(cap)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard app.storage.save(image: image, for: cap, version: version) else {
|
||||||
|
log("Failed to save image \(version) for cap \(cap) to disk")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
guard update(count: version + 1, for: cap) else {
|
||||||
|
log("Failed update count \(version) for cap \(cap)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
listeners.forEach { $0.value?.database(didChangeCap: cap) }
|
||||||
|
|
||||||
|
guard addPendingUpload(for: cap, version: version) else {
|
||||||
|
log("Failed to add cap \(cap) version \(version) to upload queue")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
upload.uploadImage(for: cap, version: version) { count in
|
||||||
|
guard let _ = count else {
|
||||||
|
self.log("Failed to upload image \(version) for cap \(cap)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard self.removePendingUpload(of: cap, version: version) else {
|
||||||
|
self.log("Failed to remove version \(version) for cap \(cap) from upload queue")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.log("Uploaded version \(version) for cap \(cap)")
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Updating cap properties
|
||||||
|
|
||||||
|
private func update(_ property: String, for cap: Int, setter: Setter...) -> Bool {
|
||||||
|
do {
|
||||||
|
let query = updateQuery(for: cap).update(setter)
|
||||||
|
try db.run(query)
|
||||||
|
listeners.forEach { $0.value?.database(didChangeCap: cap) }
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
log("Failed to update \(property) for cap \(cap): \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
private func update(uploaded: Bool, for cap: Int) -> Bool {
|
||||||
|
update("uploaded", for: cap, setter: Cap.rowUploaded <- uploaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func update(name: String, for cap: Int) -> Bool {
|
||||||
|
update("name", for: cap, setter: Cap.rowName <- name)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func update(color: UIColor, for cap: Int) -> Bool {
|
||||||
|
let (red, green, blue) = color.rgb
|
||||||
|
return update("color", for: cap, setter: Cap.rowRed <- red, Cap.rowGreen <- green, Cap.rowBlue <- blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func update(tile: Int, for cap: Int) -> Bool {
|
||||||
|
update("tile", for: cap, setter: Cap.rowTile <- tile)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func update(count: Int, for cap: Int) -> Bool {
|
||||||
|
update("count", for: cap, setter: Cap.rowCount <- count)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func update(matched: Bool, for cap: Int) -> Bool {
|
||||||
|
update("matched", for: cap, setter: Cap.rowMatched <- matched)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(recognizedCaps: Set<Int>) {
|
||||||
|
let unrecognized = self.unmatchedCaps
|
||||||
|
// Update caps which haven't been recognized before
|
||||||
|
let newlyRecognized = recognizedCaps.intersection(unrecognized)
|
||||||
|
let logIndividualMessages = newlyRecognized.count < 10
|
||||||
|
if !logIndividualMessages {
|
||||||
|
log("Marking \(newlyRecognized.count) caps as matched")
|
||||||
|
}
|
||||||
|
for cap in newlyRecognized {
|
||||||
|
if logIndividualMessages {
|
||||||
|
log("Marking cap \(cap) as matched")
|
||||||
|
}
|
||||||
|
update(matched: true, for: cap)
|
||||||
|
}
|
||||||
|
// Update caps which are no longer recognized
|
||||||
|
let missing = Set(1...capCount).subtracting(recognizedCaps).subtracting(unrecognized)
|
||||||
|
for cap in missing {
|
||||||
|
log("Marking cap \(cap) as not matched")
|
||||||
|
update(matched: false, for: cap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPendingUpload(for cap: Int, version: Int) -> Bool {
|
||||||
|
do {
|
||||||
|
try db.run(upload.insertQuery(for: cap, version: version))
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
log("Failed to add pending upload of cap \(cap) version \(version): \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removePendingUpload(for cap: Int, version: Int) -> Bool {
|
||||||
|
do {
|
||||||
|
try db.run(upload.deleteQuery(for: cap, version: version))
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
log("Failed to remove pending upload of cap \(cap) version \(version): \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Information retrieval
|
||||||
|
|
||||||
|
func cap(for id: Int) -> Cap? {
|
||||||
|
do {
|
||||||
|
guard let row = try db.pluck(updateQuery(for: id)) else {
|
||||||
|
log("No cap with id \(id) in database")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return Cap(row: row)
|
||||||
|
} catch {
|
||||||
|
log("Failed to get cap \(id): \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func count(for cap: Int) -> Int? {
|
||||||
|
do {
|
||||||
|
let row = try db.pluck(updateQuery(for: cap).select(Cap.rowCount))
|
||||||
|
return row?[Cap.rowCount]
|
||||||
|
} catch {
|
||||||
|
log("Failed to get count for cap \(cap)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func countOfCaps(withImageCountLessThan limit: Int) -> Int {
|
||||||
|
do {
|
||||||
|
return try db.scalar(Cap.table.filter(Cap.rowCount < limit).count)
|
||||||
|
} catch {
|
||||||
|
log("Failed to get caps with less than \(limit) images")
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lowestImageCountForCaps(startingAt start: Int) -> (count: Int, numberOfCaps: Int) {
|
||||||
|
do {
|
||||||
|
var currentCount = start - 1
|
||||||
|
var capsFound = 0
|
||||||
|
repeat {
|
||||||
|
currentCount += 1
|
||||||
|
capsFound = try db.scalar(Cap.table.filter(Cap.rowCount == currentCount).count)
|
||||||
|
} while capsFound == 0
|
||||||
|
|
||||||
|
return (currentCount, capsFound)
|
||||||
|
} catch {
|
||||||
|
return (0,0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateQuery(for cap: Int) -> Table {
|
||||||
|
Cap.table.filter(Cap.rowId == cap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Downloads
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func downloadMainImage(for cap: Int, completion: @escaping (_ image: UIImage?) -> Void) -> Bool {
|
||||||
|
return download.image(for: cap, version: 0) { image in
|
||||||
|
// Guaranteed to be on the main queue
|
||||||
|
guard let image = image else {
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer {
|
||||||
|
completion(image)
|
||||||
|
}
|
||||||
|
if !app.storage.save(thumbnail: Cap.thumbnail(for: image), for: cap) {
|
||||||
|
self.log("Failed to save thumbnail for cap \(cap)")
|
||||||
|
}
|
||||||
|
guard let color = image.averageColor else {
|
||||||
|
self.log("Failed to calculate color for cap \(cap)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.update(color: color, for: cap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func downloadImage(for cap: Int, version: Int, completion: @escaping (_ image: UIImage?) -> Void) -> Bool {
|
||||||
|
return download.image(for: cap, version: version, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServerDatabaseSize(completion: @escaping (_ size: Int64?) -> Void) {
|
||||||
|
download.databaseSize(completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadServerDatabase(progress: Download.Delegate.ProgressHandler? = nil, completion: @escaping (_ success: Bool) -> Void, processed: (() -> Void)? = nil) {
|
||||||
|
download.database(progress: progress) { tempUrl in
|
||||||
|
guard let url = tempUrl else {
|
||||||
|
self.log("Failed to download database")
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion(true)
|
||||||
|
self.processServerDatabase(at: url)
|
||||||
|
processed?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadMainCapImages(progress: @escaping (_ current: Int, _ total: Int) -> Void) {
|
||||||
|
let caps = self.caps.filter { !$0.hasImage }.map { $0.id }
|
||||||
|
|
||||||
|
var downloaded = 0
|
||||||
|
let total = caps.count
|
||||||
|
|
||||||
|
func update() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
progress(downloaded, total)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
update()
|
||||||
|
|
||||||
|
guard total > 0 else {
|
||||||
|
log("No images to download")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log("Starting to download \(total) images")
|
||||||
|
|
||||||
|
let group = DispatchGroup()
|
||||||
|
let split = 50
|
||||||
|
DispatchQueue.global(qos: .userInitiated).async {
|
||||||
|
for part in caps.split(intoPartsOf: split) {
|
||||||
|
for id in part {
|
||||||
|
let downloading = self.downloadMainImage(for: id) { _ in
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
if downloading {
|
||||||
|
group.enter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if group.wait(timeout: .now() + .seconds(30)) != .success {
|
||||||
|
self.log("Timed out waiting for images to be downloaded")
|
||||||
|
}
|
||||||
|
downloaded += part.count
|
||||||
|
self.log("Finished \(downloaded) of \(total) image downloads")
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
self.log("Finished all image downloads")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasNewClassifier(completion: @escaping (_ version: Int?, _ size: Int64?) -> Void) {
|
||||||
|
download.classifierVersion { version in
|
||||||
|
guard let version = version else {
|
||||||
|
self.log("Failed to download server model version")
|
||||||
|
completion(nil, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let ownVersion = self.classifierVersion
|
||||||
|
guard ownVersion < version else {
|
||||||
|
self.log("Not updating classifier: Own version \(ownVersion), server version \(version)")
|
||||||
|
completion(nil, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.log("Getting classifier size: Own version \(ownVersion), server version \(version)")
|
||||||
|
self.download.classifierSize { size in
|
||||||
|
completion(version, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadClassifier(progress: Download.Delegate.ProgressHandler? = nil, completion: @escaping (_ success: Bool) -> Void) {
|
||||||
|
download.classifier(progress: progress) { url in
|
||||||
|
guard let url = url else {
|
||||||
|
self.log("Failed to download classifier")
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let compiledUrl: URL
|
||||||
|
do {
|
||||||
|
compiledUrl = try MLModel.compileModel(at: url)
|
||||||
|
} catch {
|
||||||
|
self.log("Failed to compile downloaded classifier: \(error)")
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard app.storage.save(recognitionModelAt: compiledUrl) else {
|
||||||
|
self.log("Failed to save classifier")
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion(true)
|
||||||
|
self.download.classifierVersion { version in
|
||||||
|
guard let version = version else {
|
||||||
|
self.log("Failed to download classifier version")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.classifierVersion = version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadImageCounts() {
|
||||||
|
guard !hasPendingUploads else {
|
||||||
|
log("Waiting to refresh server image counts (uploads pending)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log("Refreshing server image counts")
|
||||||
|
app.database.download.imageCounts { counts in
|
||||||
|
guard let counts = counts else {
|
||||||
|
self.log("Failed to download server image counts")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.didDownload(imageCounts: counts)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func didDownload(imageCounts newCounts: [(cap: Int, count: Int)]) {
|
||||||
|
let capsCounts = self.caps.reduce(into: [:]) { $0[$1.id] = $1.count }
|
||||||
|
if newCounts.count != capsCounts.count {
|
||||||
|
log("Downloaded \(newCounts.count) image counts, but \(app.database.capCount) caps stored locally")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let changed = newCounts.compactMap { id, newCount -> Int? in
|
||||||
|
guard let oldCount = capsCounts[id] else {
|
||||||
|
log("Received count \(newCount) for unknown cap \(id)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard oldCount != newCount else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
app.database.update(count: newCount, for: id)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
switch changed.count {
|
||||||
|
case 0:
|
||||||
|
log("Refreshed image counts for all caps without changes")
|
||||||
|
case 1:
|
||||||
|
log("Refreshed image counts for caps, changed cap \(changed[0])")
|
||||||
|
case 2...10:
|
||||||
|
log("Refreshed image counts for caps \(changed.map(String.init).joined(separator: ", ")).")
|
||||||
|
default:
|
||||||
|
log("Refreshed image counts for all caps (\(changed.count) changed)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processServerDatabase(at url: URL) {
|
||||||
|
guard let db = ServerDatabase(downloadedTo: url) else {
|
||||||
|
log("Failed to open downloaded server database")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for (id, count, name) in db.caps {
|
||||||
|
let cap = Cap(id: id, name: name, count: count)
|
||||||
|
insert(cap: cap, notifyDelegate: false)
|
||||||
|
}
|
||||||
|
listeners.forEach { $0.value?.databaseRequiresFullRefresh() }
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadRemainingImages() {
|
||||||
|
guard pendingUploads.count > 0 else {
|
||||||
|
log("No pending uploads")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log("\(pendingUploads.count) image uploads pending")
|
||||||
|
|
||||||
|
for (cap, version) in pendingUploads {
|
||||||
|
upload.uploadImage(for: cap, version: version) { count in
|
||||||
|
guard let _ = count else {
|
||||||
|
self.log("Failed to upload version \(version) of cap \(cap)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.log("Uploaded version \(version) of cap \(cap)")
|
||||||
|
self.removePendingUpload(of: cap, version: version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func removePendingUpload(of cap: Int, version: Int) -> Bool {
|
||||||
|
do {
|
||||||
|
let query = upload.table.filter(upload.rowCapId == cap && upload.rowCapVersion == version).delete()
|
||||||
|
try db.run(query)
|
||||||
|
log("Deleted pending upload of cap \(cap) version \(version)")
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
log("Failed to delete pending upload of cap \(cap) version \(version)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setMainImage(of cap: Int, to version: Int) {
|
||||||
|
guard version != 0 else {
|
||||||
|
log("No need to switch main image with itself for cap \(cap)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
upload.setMainImage(for: cap, to: version) { color in
|
||||||
|
guard let color = color else {
|
||||||
|
self.log("Could not make \(version) the main image for cap \(cap)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.update(color: color, for: cap)
|
||||||
|
self.listeners.forEach { $0.value?.database(didChangeCap: cap) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Database: Logger { }
|
@ -1,207 +0,0 @@
|
|||||||
//
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func imageUrlForCap(_ id: Int) -> URL? {
|
|
||||||
let url = localUrl(for: id)
|
|
||||||
guard fm.fileExists(atPath: url.path) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
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]"
|
|
||||||
}
|
|
328
CapCollector/Data/Download.swift
Normal file
328
CapCollector/Data/Download.swift
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
//
|
||||||
|
// Download.swift
|
||||||
|
// CapCollector
|
||||||
|
//
|
||||||
|
// Created by Christoph on 26.04.20.
|
||||||
|
// Copyright © 2020 CH. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class Download {
|
||||||
|
|
||||||
|
let serverUrl: URL
|
||||||
|
|
||||||
|
let session: URLSession
|
||||||
|
|
||||||
|
let delegate: Delegate
|
||||||
|
|
||||||
|
private var downloadingMainImages = Set<Int>()
|
||||||
|
|
||||||
|
init(server: URL) {
|
||||||
|
let delegate = Delegate()
|
||||||
|
|
||||||
|
self.serverUrl = server
|
||||||
|
self.session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
|
||||||
|
self.delegate = delegate
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Paths
|
||||||
|
|
||||||
|
private static func serverDatabaseUrl(server: URL) -> URL {
|
||||||
|
server.appendingPathComponent("db.sqlite3")
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverDatabaseUrl: URL {
|
||||||
|
Download.serverDatabaseUrl(server: serverUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverImageUrl: URL {
|
||||||
|
serverUrl.appendingPathComponent("images")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func serverImageUrl(for cap: Int, version: Int = 0) -> URL {
|
||||||
|
serverImageUrl.appendingPathComponent(String(format: "%04d/%04d-%02d.jpg", cap, cap, version))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func serverImageCountUrl(for cap: Int) -> URL {
|
||||||
|
serverUrl.appendingPathComponent("count/\(cap)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private var serverClassifierVersionUrl: URL {
|
||||||
|
serverUrl.appendingPathComponent("classifier.version")
|
||||||
|
}
|
||||||
|
|
||||||
|
private var serverAllCountsUrl: URL {
|
||||||
|
serverUrl.appendingPathComponent("count/all")
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverRecognitionModelUrl: URL {
|
||||||
|
serverUrl.appendingPathComponent("classifier.mlmodel")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Delegate
|
||||||
|
|
||||||
|
final class Delegate: NSObject, URLSessionDownloadDelegate {
|
||||||
|
|
||||||
|
typealias ProgressHandler = (_ progress: Float, _ bytesWritten: Int64, _ totalBytes: Int64) -> Void
|
||||||
|
|
||||||
|
typealias CompletionHandler = (_ url: URL?) -> Void
|
||||||
|
|
||||||
|
private var progress = [URLSessionDownloadTask : Float]()
|
||||||
|
|
||||||
|
private var callbacks = [URLSessionDownloadTask : ProgressHandler]()
|
||||||
|
|
||||||
|
private var completions = [URLSessionDownloadTask : CompletionHandler]()
|
||||||
|
|
||||||
|
func registerForProgress(_ downloadTask: URLSessionDownloadTask, callback: ProgressHandler?, completion: @escaping CompletionHandler) {
|
||||||
|
progress[downloadTask] = 0
|
||||||
|
callbacks[downloadTask] = callback
|
||||||
|
completions[downloadTask] = completion
|
||||||
|
}
|
||||||
|
|
||||||
|
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||||
|
completions[downloadTask]?(location)
|
||||||
|
callbacks[downloadTask] = nil
|
||||||
|
progress[downloadTask] = nil
|
||||||
|
completions[downloadTask] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||||
|
let ratio = totalBytesExpectedToWrite > 0 ? Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) : 0
|
||||||
|
progress[downloadTask] = ratio
|
||||||
|
callbacks[downloadTask]?(ratio, totalBytesWritten, totalBytesExpectedToWrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||||
|
guard let downloadTask = task as? URLSessionDownloadTask else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completions[downloadTask]?(nil)
|
||||||
|
callbacks[downloadTask] = nil
|
||||||
|
progress[downloadTask] = nil
|
||||||
|
completions[downloadTask] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Downloading data
|
||||||
|
|
||||||
|
/**
|
||||||
|
Download an image for a cap.
|
||||||
|
- Parameter cap: The id of the cap.
|
||||||
|
- Parameter version: The image version to download.
|
||||||
|
- Parameter completion: A closure with the resulting image
|
||||||
|
- Note: The closure will be called from the main queue.
|
||||||
|
- Returns: `true`, of the file download was started, `false`, if the image is already downloading.
|
||||||
|
*/
|
||||||
|
@discardableResult
|
||||||
|
func mainImage(for cap: Int, completion: ((_ image: UIImage?) -> Void)?) -> Bool {
|
||||||
|
let url = serverImageUrl(for: cap)
|
||||||
|
let query = "Main image of cap \(cap)"
|
||||||
|
guard !downloadingMainImages.contains(cap) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
downloadingMainImages.insert(cap)
|
||||||
|
|
||||||
|
let task = session.downloadTask(with: url) { fileUrl, response, error in
|
||||||
|
self.downloadingMainImages.remove(cap)
|
||||||
|
guard let fileUrl = self.convertResponse(to: query, fileUrl, response, error) else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion?(nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard app.storage.saveImage(at: fileUrl, for: cap) else {
|
||||||
|
self.log("Request '\(query)' could not move downloaded file")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion?(nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let image = app.storage.image(for: cap) else {
|
||||||
|
self.log("Request '\(query)' received an invalid image")
|
||||||
|
completion?(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion?(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Download an image for a cap.
|
||||||
|
- Parameter cap: The id of the cap.
|
||||||
|
- Parameter version: The image version to download.
|
||||||
|
- Parameter completion: A closure with the resulting image
|
||||||
|
- Note: The closure will be called from the main queue.
|
||||||
|
- Returns: `true`, of the file download was started, `false`, if the image is already downloading.
|
||||||
|
*/
|
||||||
|
@discardableResult
|
||||||
|
func image(for cap: Int, version: Int, completion: @escaping (_ image: UIImage?) -> Void) -> Bool {
|
||||||
|
let url = serverImageUrl(for: cap, version: version)
|
||||||
|
let query = "Image of cap \(cap) version \(version)"
|
||||||
|
let task = session.downloadTask(with: url) { fileUrl, response, error in
|
||||||
|
guard let fileUrl = self.convertResponse(to: query, fileUrl, response, error) else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard app.storage.saveImage(at: fileUrl, for: cap, version: version) else {
|
||||||
|
self.log("Request '\(query)' could not move downloaded file")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(nil)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let image = app.storage.image(for: cap, version: version) else {
|
||||||
|
self.log("Request '\(query)' received an invalid image")
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageCount(for cap: Int, completion: @escaping (_ count: Int?) -> Void) {
|
||||||
|
let url = serverImageCountUrl(for: cap)
|
||||||
|
let query = "Image count for cap \(cap)"
|
||||||
|
let task = session.dataTask(with: url) { data, response, error in
|
||||||
|
let int = self.convertIntResponse(to: query, data, response, error)
|
||||||
|
completion(int)
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageCounts(completion: @escaping ([(cap: Int, count: Int)]?) -> Void) {
|
||||||
|
let url = serverAllCountsUrl
|
||||||
|
let query = "Image count of all caps"
|
||||||
|
let task = session.dataTask(with: url) { data, response, error in
|
||||||
|
guard let string = self.convertStringResponse(to: query, data, response, error) else {
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the encoded string into (id, count) pairs
|
||||||
|
let parts = string.components(separatedBy: ";")
|
||||||
|
let array: [(cap: Int, count: Int)] = parts.compactMap { s in
|
||||||
|
let p = s.components(separatedBy: "#")
|
||||||
|
guard p.count == 2, let cap = Int(p[0]), let count = Int(p[1]) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return (cap, count)
|
||||||
|
}
|
||||||
|
completion(array)
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
func databaseSize(completion: @escaping (_ size: Int64?) -> Void) {
|
||||||
|
size(of: "database size", to: serverDatabaseUrl, completion: completion)
|
||||||
|
}
|
||||||
|
func database(progress: Delegate.ProgressHandler? = nil, completion: @escaping (URL?) -> Void) {
|
||||||
|
//let query = "Download of server database"
|
||||||
|
let task = session.downloadTask(with: serverDatabaseUrl)
|
||||||
|
delegate.registerForProgress(task, callback: progress) {url in
|
||||||
|
self.log("Database download complete")
|
||||||
|
completion(url)
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func classifierVersion(completion: @escaping (Int?) -> Void) {
|
||||||
|
let query = "Server classifier version"
|
||||||
|
let task = session.dataTask(with: serverClassifierVersionUrl) { data, response, error in
|
||||||
|
let int = self.convertIntResponse(to: query, data, response, error)
|
||||||
|
completion(int)
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
func classifierSize(completion: @escaping (Int64?) -> Void) {
|
||||||
|
size(of: "classifier size", to: serverRecognitionModelUrl, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func classifier(progress: Delegate.ProgressHandler? = nil, completion: @escaping (URL?) -> Void) {
|
||||||
|
let task = session.downloadTask(with: serverRecognitionModelUrl)
|
||||||
|
delegate.registerForProgress(task, callback: progress) { url in
|
||||||
|
self.log("Classifier download complete")
|
||||||
|
completion(url)
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Requests
|
||||||
|
|
||||||
|
private func size(of query: String, to url: URL, completion: @escaping (_ size: Int64?) -> Void) {
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "HEAD"
|
||||||
|
let task = session.dataTask(with: request) { _, response, _ in
|
||||||
|
guard let r = response else {
|
||||||
|
self.log("Request '\(query)' received no response")
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion(r.expectedContentLength)
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func convertIntResponse(to query: String, _ data: Data?, _ response: URLResponse?, _ error: Error?) -> Int? {
|
||||||
|
guard let string = self.convertStringResponse(to: query, data, response, error) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let int = Int(string.trimmingCharacters(in: .whitespacesAndNewlines)) else {
|
||||||
|
self.log("Request '\(query)' received an invalid value '\(string)'")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return int
|
||||||
|
}
|
||||||
|
|
||||||
|
private func convertStringResponse(to query: String, _ data: Data?, _ response: URLResponse?, _ error: Error?) -> String? {
|
||||||
|
guard let data = self.convertResponse(to: query, data, response, error) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let string = String(data: data, encoding: .utf8) else {
|
||||||
|
self.log("Request '\(query)' received invalid data (not a string)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return string
|
||||||
|
}
|
||||||
|
|
||||||
|
private func convertResponse<T>(to query: String, _ result: T?, _ response: URLResponse?, _ error: Error?) -> T? {
|
||||||
|
if let error = error {
|
||||||
|
log("Request '\(query)' produced an error: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let response = response else {
|
||||||
|
log("Request '\(query)' received no response")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let urlResponse = response as? HTTPURLResponse else {
|
||||||
|
log("Request '\(query)' received an invalid response: \(response)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard urlResponse.statusCode == 200 else {
|
||||||
|
log("Request '\(query)' failed with status code \(urlResponse.statusCode)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let r = result else {
|
||||||
|
log("Request '\(query)' received no item")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Download: Logger { }
|
@ -1,145 +0,0 @@
|
|||||||
//
|
|
||||||
// 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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
47
CapCollector/Data/ServerDatabase.swift
Normal file
47
CapCollector/Data/ServerDatabase.swift
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
//
|
||||||
|
// ServerDatabase.swift
|
||||||
|
// CapCollector
|
||||||
|
//
|
||||||
|
// Created by Christoph on 27.04.20.
|
||||||
|
// Copyright © 2020 CH. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SQLite
|
||||||
|
|
||||||
|
final class ServerDatabase {
|
||||||
|
|
||||||
|
let db: Connection
|
||||||
|
|
||||||
|
var table: Table {
|
||||||
|
Table("caps")
|
||||||
|
}
|
||||||
|
|
||||||
|
let rowId = Expression<Int>("id")
|
||||||
|
|
||||||
|
let rowName = Expression<String>("name")
|
||||||
|
|
||||||
|
let rowCount = Expression<Int>("count")
|
||||||
|
|
||||||
|
init?(downloadedTo url: URL) {
|
||||||
|
guard let db = try? Connection(url.path) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.db = db
|
||||||
|
log("Server database loaded with \(capCount) caps")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The number of caps currently in the database
|
||||||
|
var capCount: Int {
|
||||||
|
(try? db.scalar(table.count)) ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var caps: [(id: Int, count: Int, name: String)] {
|
||||||
|
guard let rows = try? db.prepare(table) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return rows.map { ($0[rowId], $0[rowCount], $0[rowName]) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ServerDatabase: Logger { }
|
321
CapCollector/Data/Storage.swift
Normal file
321
CapCollector/Data/Storage.swift
Normal file
@ -0,0 +1,321 @@
|
|||||||
|
//
|
||||||
|
// DiskManager.swift
|
||||||
|
// CapFinder
|
||||||
|
//
|
||||||
|
// Created by User on 23.04.18.
|
||||||
|
// Copyright © 2018 User. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import CoreML
|
||||||
|
import Vision
|
||||||
|
|
||||||
|
final class Storage {
|
||||||
|
|
||||||
|
// MARK: Paths
|
||||||
|
|
||||||
|
let fm = FileManager.default
|
||||||
|
|
||||||
|
let baseUrl: URL
|
||||||
|
|
||||||
|
// MARK: INIT
|
||||||
|
|
||||||
|
init(in folder: URL) {
|
||||||
|
self.baseUrl = folder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: File/folder urls
|
||||||
|
|
||||||
|
var dbUrl: URL {
|
||||||
|
baseUrl.appendingPathComponent("db.sqlite3")
|
||||||
|
}
|
||||||
|
|
||||||
|
var modelUrl: URL {
|
||||||
|
baseUrl.appendingPathComponent("model.mlmodel")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func localImageUrl(for cap: Int, version: Int) -> URL {
|
||||||
|
baseUrl.appendingPathComponent("\(cap)-\(version).jpg")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func thumbnailUrl(for cap: Int) -> URL {
|
||||||
|
baseUrl.appendingPathComponent("\(cap)-thumb.jpg")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Storage
|
||||||
|
|
||||||
|
/**
|
||||||
|
Save an image to disk
|
||||||
|
- parameter url: The url where the downloaded image is stored
|
||||||
|
- parameter cap: The cap id
|
||||||
|
- parameter version: The version of the image to get
|
||||||
|
- returns: True, if the image was saved
|
||||||
|
*/
|
||||||
|
func saveImage(at url: URL, for cap: Int, version: Int = 0) -> Bool {
|
||||||
|
let targetUrl = localImageUrl(for: cap, version: version)
|
||||||
|
do {
|
||||||
|
if fm.fileExists(atPath: targetUrl.path) {
|
||||||
|
try fm.removeItem(at: targetUrl)
|
||||||
|
}
|
||||||
|
try fm.moveItem(at: url, to: targetUrl)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
log("Failed to delete or move image \(version) for cap \(cap)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Save an image to disk
|
||||||
|
- parameter image: The image
|
||||||
|
- parameter cap: The cap id
|
||||||
|
- parameter version: The version of the image
|
||||||
|
- returns: True, if the image was saved
|
||||||
|
*/
|
||||||
|
func save(image: UIImage, for cap: Int, version: Int = 0) -> Bool {
|
||||||
|
guard let data = image.jpegData(compressionQuality: Cap.jpgQuality) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return save(imageData: data, for: cap, version: version)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Save image data to disk
|
||||||
|
- parameter image: The data of the image
|
||||||
|
- parameter cap: The cap id
|
||||||
|
- parameter version: The version of the image
|
||||||
|
- returns: True, if the image was saved
|
||||||
|
*/
|
||||||
|
func save(imageData: Data, for cap: Int, version: Int = 0) -> Bool {
|
||||||
|
write(imageData, to: localImageUrl(for: cap, version: version))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Save a thumbnail.
|
||||||
|
- parameter thumbnail: The image
|
||||||
|
- parameter cap: The cap id
|
||||||
|
- returns: True, if the image was saved
|
||||||
|
*/
|
||||||
|
func save(thumbnail: UIImage, for cap: Int) -> Bool {
|
||||||
|
guard let data = thumbnail.jpegData(compressionQuality: Cap.jpgQuality) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return save(thumbnailData: data, for: cap)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
func save(thumbnailData: Data, for cap: Int) -> Bool {
|
||||||
|
write(thumbnailData, to: thumbnailUrl(for: cap))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Save the downloaded and compiled recognition model.
|
||||||
|
- Parameter url: The temporary location to which the model was compiled.
|
||||||
|
- Returns: `true`, if the model was moved.
|
||||||
|
*/
|
||||||
|
func save(recognitionModelAt url: URL) -> Bool {
|
||||||
|
move(url, to: modelUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Save the downloaded and database.
|
||||||
|
- Parameter url: The temporary location to which the database was downloaded.
|
||||||
|
- Returns: `true`, if the database was moved.
|
||||||
|
*/
|
||||||
|
func save(databaseAt url: URL) -> Bool {
|
||||||
|
move(url, to: dbUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func move(_ url: URL, to destination: URL) -> Bool {
|
||||||
|
if fm.fileExists(atPath: destination.path) {
|
||||||
|
do {
|
||||||
|
try fm.removeItem(at: destination)
|
||||||
|
} catch {
|
||||||
|
log("Failed to remove file \(destination.lastPathComponent) before writing new version: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try fm.moveItem(at: url, to: destination)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
self.error("Failed to move file \(destination.lastPathComponent) to permanent location: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Status
|
||||||
|
|
||||||
|
/**
|
||||||
|
Check if an image exists for a cap
|
||||||
|
- parameter cap: The id of the cap
|
||||||
|
- returns: True, if an image exists
|
||||||
|
*/
|
||||||
|
func hasImage(for cap: Int) -> Bool {
|
||||||
|
fm.fileExists(atPath: localImageUrl(for: cap, version: 0).path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func existingImageUrl(for cap: Int, version: Int = 0) -> URL? {
|
||||||
|
let url = localImageUrl(for: cap, version: version)
|
||||||
|
return fm.fileExists(atPath: url.path) ? url : nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Retrieval
|
||||||
|
|
||||||
|
/**
|
||||||
|
Get the image data 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
|
||||||
|
- parameter version: The image version
|
||||||
|
- returns: The image data, or `nil`
|
||||||
|
*/
|
||||||
|
func imageData(for cap: Int, version: Int = 0) -> Data? {
|
||||||
|
readData(from: localImageUrl(for: cap, version: version))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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
|
||||||
|
- parameter version: The image version
|
||||||
|
- returns: The image, or `nil`
|
||||||
|
- note: Removes invalid image data on disk, if the data is not a valid image
|
||||||
|
- note: Must be called on the main thread
|
||||||
|
*/
|
||||||
|
func image(for cap: Int, version: Int = 0) -> UIImage? {
|
||||||
|
guard let data = imageData(for: cap, version: version) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let image = UIImage(data: data) else {
|
||||||
|
log("Removing invalid image \(version) of cap \(cap) from disk")
|
||||||
|
deleteImage(for: cap, version: version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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
|
||||||
|
- parameter version: The image version
|
||||||
|
- returns: The image, or `nil`
|
||||||
|
*/
|
||||||
|
func ciImage(for cap: Int, version: Int = 0) -> CIImage? {
|
||||||
|
guard let url = existingImageUrl(for: cap, version: version) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return CIImage(contentsOf: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Get the thumbnail data 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`
|
||||||
|
*/
|
||||||
|
func thumbnailData(for cap: Int) -> Data? {
|
||||||
|
readData(from: thumbnailUrl(for: cap))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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, or `nil`
|
||||||
|
*/
|
||||||
|
func thumbnail(for cap: Int) -> UIImage? {
|
||||||
|
guard let data = thumbnailData(for: cap) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return UIImage(data: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The compiled recognition model on disk
|
||||||
|
var recognitionModel: VNCoreMLModel? {
|
||||||
|
guard fm.fileExists(atPath: modelUrl.path) else {
|
||||||
|
log("No recognition model to load")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let model = try MLModel(contentsOf: modelUrl)
|
||||||
|
|
||||||
|
return try VNCoreMLModel(for: model)
|
||||||
|
} catch {
|
||||||
|
self.error("Failed to load recognition model: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func averageColor(for cap: Int, version: Int = 0) -> UIColor? {
|
||||||
|
guard let inputImage = ciImage(for: cap, version: version) else {
|
||||||
|
error("Failed to read CIImage for main image of cap \(cap)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return inputImage.averageColor
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Deleting data
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func deleteDatabase() -> Bool {
|
||||||
|
do {
|
||||||
|
try fm.removeItem(at: dbUrl)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
log("Failed to delete database: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func deleteImage(for cap: Int, version: Int) -> Bool {
|
||||||
|
guard let url = existingImageUrl(for: cap, version: version) else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try fm.removeItem(at: url)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
log("Failed to delete image \(version) for cap \(cap): \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Storage: Logger { }
|
||||||
|
|
180
CapCollector/Data/Upload.swift
Normal file
180
CapCollector/Data/Upload.swift
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
//
|
||||||
|
// Upload.swift
|
||||||
|
// CapCollector
|
||||||
|
//
|
||||||
|
// Created by Christoph on 26.04.20.
|
||||||
|
// Copyright © 2020 CH. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import SQLite
|
||||||
|
|
||||||
|
struct Upload {
|
||||||
|
|
||||||
|
let serverUrl: URL
|
||||||
|
|
||||||
|
let table = Table("uploads")
|
||||||
|
|
||||||
|
let rowCapId = Expression<Int>("cap")
|
||||||
|
|
||||||
|
let rowCapVersion = Expression<Int>("version")
|
||||||
|
|
||||||
|
init(server: URL) {
|
||||||
|
self.serverUrl = server
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Paths
|
||||||
|
|
||||||
|
var serverImageUrl: URL {
|
||||||
|
serverUrl.appendingPathComponent("images")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func serverImageUrl(for cap: Int, version: Int = 0) -> URL {
|
||||||
|
serverImageUrl.appendingPathComponent("\(cap)/\(cap)-\(version).jpg")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func serverImageUploadUrl(for cap: Int) -> URL {
|
||||||
|
serverImageUrl.appendingPathComponent("\(cap)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func serverNameUploadUrl(for cap: Int) -> URL {
|
||||||
|
serverUrl.appendingPathComponent("name/\(cap)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func serverChangeMainImageUrl(for cap: Int, to newValue: Int) -> URL {
|
||||||
|
serverUrl.appendingPathComponent("switch/\(cap)/\(newValue)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: SQLite
|
||||||
|
|
||||||
|
var createQuery: String {
|
||||||
|
table.create(ifNotExists: true) { t in
|
||||||
|
t.column(rowCapId)
|
||||||
|
t.column(rowCapVersion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertQuery(for cap: Int, version: Int) -> Insert {
|
||||||
|
table.insert(rowCapId <- cap, rowCapVersion <- version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteQuery(for cap: Int, version: Int) -> Delete {
|
||||||
|
table.filter(rowCapId == cap && rowCapVersion == version).delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Uploading data
|
||||||
|
|
||||||
|
func upload(name: String, for cap: Int, completion: @escaping (_ success: Bool) -> Void) {
|
||||||
|
var request = URLRequest(url: serverNameUploadUrl(for: cap))
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.httpBody = name.data(using: .utf8)
|
||||||
|
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
|
if let error = error {
|
||||||
|
self.log("Failed to upload name of cap \(cap): \(error)")
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let response = response else {
|
||||||
|
self.log("Failed to upload name of cap \(cap): No response")
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let urlResponse = response as? HTTPURLResponse else {
|
||||||
|
self.log("Failed to upload name of cap \(cap): \(response)")
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard urlResponse.statusCode == 200 else {
|
||||||
|
self.log("Failed to upload name of cap \(cap): Response \(urlResponse.statusCode)")
|
||||||
|
completion(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadImage(for cap: Int, version: Int, completion: @escaping (_ count: Int?) -> Void) {
|
||||||
|
guard let url = app.storage.existingImageUrl(for: cap, version: version) else {
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var request = URLRequest(url: serverImageUploadUrl(for: cap))
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
let task = URLSession.shared.uploadTask(with: request, fromFile: url) { data, response, error in
|
||||||
|
if let error = error {
|
||||||
|
self.log("Failed to upload image \(version) of cap \(cap): \(error)")
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let response = response else {
|
||||||
|
self.log("Failed to upload image \(version) of cap \(cap): No response")
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let urlResponse = response as? HTTPURLResponse else {
|
||||||
|
self.log("Failed to upload image \(version) of cap \(cap): \(response)")
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard urlResponse.statusCode == 200 else {
|
||||||
|
self.log("Failed to upload image \(version) of cap \(cap): Response \(urlResponse.statusCode)")
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let d = data, let string = String(data: d, encoding: .utf8), let int = Int(string) else {
|
||||||
|
self.log("Failed to upload image \(version) of cap \(cap): Invalid response")
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completion(int)
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Sets the main image for a cap to a different version.
|
||||||
|
- Parameter cap: The id of the cap
|
||||||
|
- Parameter version: The version to set as the main version.
|
||||||
|
- Parameter completion: A callback with the new average color on completion.
|
||||||
|
*/
|
||||||
|
func setMainImage(for cap: Int, to version: Int, completion: @escaping (_ averageColor: UIColor?) -> Void) {
|
||||||
|
guard let averageColor = app.storage.averageColor(for: cap, version: version) else {
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let url = serverChangeMainImageUrl(for: cap, to: version)
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
let averageRGB = averageColor.rgb
|
||||||
|
request.addValue("\(averageRGB.red)", forHTTPHeaderField: "r")
|
||||||
|
request.addValue("\(averageRGB.green)", forHTTPHeaderField: "g")
|
||||||
|
request.addValue("\(averageRGB.blue)", forHTTPHeaderField: "b")
|
||||||
|
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||||
|
if let error = error {
|
||||||
|
self.log("Failed to set main image of cap \(cap) to \(version): \(error)")
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let response = response else {
|
||||||
|
self.log("Failed to set main image of cap \(cap) to \(version): No response")
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let urlResponse = response as? HTTPURLResponse else {
|
||||||
|
self.log("Failed to set main image of cap \(cap) to \(version): \(response)")
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard urlResponse.statusCode == 200 else {
|
||||||
|
self.log("Failed to set main image of cap \(cap) to \(version): Response \(urlResponse.statusCode)")
|
||||||
|
completion(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
completion(averageColor)
|
||||||
|
}
|
||||||
|
task.resume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Upload: Logger { }
|
@ -1,67 +0,0 @@
|
|||||||
//
|
|
||||||
// UserDefaults.swift
|
|
||||||
// CapCollector
|
|
||||||
//
|
|
||||||
// Created by Christoph on 16.10.18.
|
|
||||||
// Copyright © 2018 CH. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
final class Persistence {
|
|
||||||
|
|
||||||
static var recognizedCapCount: Int {
|
|
||||||
get {
|
|
||||||
return UserDefaults.standard.integer(forKey: "recognizedCaps")
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
UserDefaults.standard.set(newValue, forKey: "recognizedCaps")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static var newImageCount: Int {
|
|
||||||
get {
|
|
||||||
return UserDefaults.standard.integer(forKey: "newImages")
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
UserDefaults.standard.set(newValue, forKey: "newImages")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static var lastUploadedCapCount: Int {
|
|
||||||
get {
|
|
||||||
return UserDefaults.standard.integer(forKey: "lastUploadedCaps")
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
UserDefaults.standard.set(newValue, forKey: "lastUploadedCaps")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static var lastUploadedImageCount: Int {
|
|
||||||
get {
|
|
||||||
return UserDefaults.standard.integer(forKey: "lastUploadedImages")
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
UserDefaults.standard.set(newValue, forKey: "lastUploadedImages")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static var useMobileNet: Bool {
|
|
||||||
get {
|
|
||||||
return UserDefaults.standard.bool(forKey: "mobileNet")
|
|
||||||
}
|
|
||||||
|
|
||||||
set {
|
|
||||||
UserDefaults.standard.set(newValue, forKey: "mobileNet")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static var folderNotCreated: [Int] {
|
|
||||||
get {
|
|
||||||
return UserDefaults.standard.array(forKey: "folders") as? [Int] ?? []
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
UserDefaults.standard.set(newValue, forKey: "folders")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
30
CapCollector/Extensions/Array+Extensions.swift
Normal file
30
CapCollector/Extensions/Array+Extensions.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// Array+Extensions.swift
|
||||||
|
// CapCollector
|
||||||
|
//
|
||||||
|
// Created by Christoph on 12.05.20.
|
||||||
|
// Copyright © 2020 CH. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Array {
|
||||||
|
|
||||||
|
func split(intoPartsOf maxElements: Int) -> [ArraySlice<Element>] {
|
||||||
|
guard !isEmpty, maxElements > 0 else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
var result = [ArraySlice<Element>]()
|
||||||
|
var currentIndex = 0
|
||||||
|
while true {
|
||||||
|
let nextIndex = currentIndex + maxElements
|
||||||
|
if nextIndex >= count {
|
||||||
|
result.append(self[currentIndex..<count])
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
result.append(self[currentIndex..<nextIndex])
|
||||||
|
currentIndex += maxElements
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
//
|
||||||
|
// CGImagePropertyOrientation+Extensions.swift
|
||||||
|
// CapCollector
|
||||||
|
//
|
||||||
|
// Created by Christoph on 13.05.20.
|
||||||
|
// Copyright © 2020 CH. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
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
|
||||||
|
@unknown default:
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
CapCollector/Extensions/UIColor+Extensions.swift
Normal file
22
CapCollector/Extensions/UIColor+Extensions.swift
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
//
|
||||||
|
// UIColor+Extensions.swift
|
||||||
|
// CapCollector
|
||||||
|
//
|
||||||
|
// Created by Christoph on 14.04.20.
|
||||||
|
// Copyright © 2020 CH. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIColor {
|
||||||
|
|
||||||
|
var rgb: (red: Int, green: Int, blue: Int) {
|
||||||
|
var fRed: CGFloat = 0
|
||||||
|
var fGreen: CGFloat = 0
|
||||||
|
var fBlue: CGFloat = 0
|
||||||
|
var fAlpha: CGFloat = 0
|
||||||
|
getRed(&fRed, green: &fGreen, blue: &fBlue, alpha: &fAlpha)
|
||||||
|
return (Int(fRed * 255.0), Int(fGreen * 255.0), Int(fBlue * 255.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -80,4 +80,48 @@ extension UIImage {
|
|||||||
UIImage(cgImage: cgImage, scale: 1, orientation: imageOrientation).draw(in: breadthRect)
|
UIImage(cgImage: cgImage, scale: 1, orientation: imageOrientation).draw(in: breadthRect)
|
||||||
return UIGraphicsGetImageFromCurrentImageContext()
|
return UIGraphicsGetImageFromCurrentImageContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var averageColor: UIColor? {
|
||||||
|
let image = ciImage ?? CIImage(cgImage: cgImage!)
|
||||||
|
return image.averageColor
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CIImage {
|
||||||
|
|
||||||
|
var averageColor: UIColor? {
|
||||||
|
let extentVector = CIVector(
|
||||||
|
x: extent.origin.x,
|
||||||
|
y: extent.origin.y,
|
||||||
|
z: extent.size.width,
|
||||||
|
w: extent.size.height)
|
||||||
|
|
||||||
|
guard let filter = CIFilter(name: "CIAreaAverage", parameters: [kCIInputImageKey: self, kCIInputExtentKey: extentVector]) else {
|
||||||
|
log("Failed to create filter")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let outputImage = filter.outputImage else {
|
||||||
|
log("Failed get filter output")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var bitmap = [UInt8](repeating: 0, count: 4)
|
||||||
|
let context = CIContext(options: [.workingColorSpace: kCFNull!])
|
||||||
|
context.render(outputImage, toBitmap: &bitmap, rowBytes: 4, bounds: CGRect(x: 0, y: 0, width: 1, height: 1), format: .RGBA8, colorSpace: nil)
|
||||||
|
|
||||||
|
let color = UIColor(
|
||||||
|
red: saturate(bitmap[0]),
|
||||||
|
green: saturate(bitmap[1]),
|
||||||
|
blue: saturate(bitmap[2]),
|
||||||
|
alpha: CGFloat(bitmap[3]) / 255)
|
||||||
|
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Map expected range 75-200 to 0-255
|
||||||
|
private func saturate(_ component: UInt8) -> CGFloat {
|
||||||
|
return max(min(CGFloat(component) * 2 - 150, 255), 0) / 255
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CIImage: Logger { }
|
31
CapCollector/Extensions/UINavigationItem+Extensions.swift
Normal file
31
CapCollector/Extensions/UINavigationItem+Extensions.swift
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
//
|
||||||
|
// UINavigationItem+Extensions.swift
|
||||||
|
// CapCollector
|
||||||
|
//
|
||||||
|
// Created by Christoph on 12.05.20.
|
||||||
|
// Copyright © 2020 CH. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UINavigationItem {
|
||||||
|
|
||||||
|
func setTitle(_ title: String, subtitle: String) {
|
||||||
|
let titleLabel = UILabel()
|
||||||
|
titleLabel.text = title
|
||||||
|
titleLabel.font = .systemFont(ofSize: 17.0)
|
||||||
|
titleLabel.textColor = .black
|
||||||
|
|
||||||
|
let subtitleLabel = UILabel()
|
||||||
|
subtitleLabel.text = subtitle
|
||||||
|
subtitleLabel.font = .systemFont(ofSize: 12.0)
|
||||||
|
subtitleLabel.textColor = .gray
|
||||||
|
|
||||||
|
let stackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
|
||||||
|
stackView.distribution = .equalCentering
|
||||||
|
stackView.alignment = .center
|
||||||
|
stackView.axis = .vertical
|
||||||
|
|
||||||
|
self.titleView = stackView
|
||||||
|
}
|
||||||
|
}
|
@ -16,4 +16,8 @@ extension UIView {
|
|||||||
subviews.forEach { subviews.append(contentsOf: $0.recursiveSubviews) }
|
subviews.forEach { subviews.append(contentsOf: $0.recursiveSubviews) }
|
||||||
return subviews
|
return subviews
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fromNib<T : UIView>() -> T { // 2
|
||||||
|
return Bundle(for: type(of: self)).loadNibNamed(String(describing: type(of: self)), owner: self, options: nil)!.first! as! T
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,8 +18,8 @@ extension UIViewController {
|
|||||||
let alertController = UIAlertController(
|
let alertController = UIAlertController(
|
||||||
title: title,
|
title: title,
|
||||||
message: message,
|
message: message,
|
||||||
preferredStyle: .alert,
|
preferredStyle: .alert)//,
|
||||||
blurStyle: .dark)
|
//blurStyle: .dark)
|
||||||
|
|
||||||
alertController.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
|
alertController.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.3</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
@ -67,7 +67,7 @@
|
|||||||
<string>armv7</string>
|
<string>armv7</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIStatusBarStyle</key>
|
<key>UIStatusBarStyle</key>
|
||||||
<string>UIStatusBarStyleLightContent</string>
|
<string>UIStatusBarStyleDefault</string>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
@ -10,26 +10,28 @@ import Foundation
|
|||||||
|
|
||||||
protocol Logger {
|
protocol Logger {
|
||||||
|
|
||||||
static var logToken: String { get }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Logger {
|
extension Logger {
|
||||||
|
|
||||||
|
static var logToken: String {
|
||||||
|
"[" + String(describing: self) + "] "
|
||||||
|
}
|
||||||
|
|
||||||
func error(_ message: String) {
|
func error(_ message: String) {
|
||||||
Self.addToFile(Self.logToken + "ERROR: " + message)
|
Self.addToFile(Self.logToken + "ERROR: " + message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func event(_ message: String) {
|
func log(_ message: String) {
|
||||||
Self.addToFile(Self.logToken + " " + message)
|
Self.addToFile(Self.logToken + message)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func error(_ message: String) {
|
static func error(_ message: String) {
|
||||||
addToFile(logToken + "ERROR: " + message)
|
addToFile(logToken + "ERROR: " + message)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func event(_ message: String) {
|
static func log(_ message: String) {
|
||||||
addToFile(logToken + " " + message)
|
addToFile(logToken + message)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func addToFile(_ message: String) {
|
private static func addToFile(_ message: String) {
|
||||||
|
@ -19,28 +19,30 @@ class CapCell: UITableViewCell {
|
|||||||
|
|
||||||
@IBOutlet weak var countLabel: UILabel!
|
@IBOutlet weak var countLabel: UILabel!
|
||||||
|
|
||||||
var id = 0
|
var id: Int = 0
|
||||||
|
|
||||||
var cap: Cap! {
|
func set(image: UIImage?) {
|
||||||
didSet {
|
capImage.image = image ?? UIImage(named: "launch")
|
||||||
updateCell()
|
}
|
||||||
|
|
||||||
|
func set(cap: Cap, match: Float?) {
|
||||||
|
id = cap.id
|
||||||
|
if let image = cap.image {
|
||||||
|
set(image: image)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
capImage.image = UIImage(named: "launch")
|
||||||
|
cap.downloadMainImage() { image in
|
||||||
|
self.set(image: image)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateCell() {
|
|
||||||
capImage.image = cap.image
|
|
||||||
//capImage.borderColor = AppDelegate.tintColor
|
//capImage.borderColor = AppDelegate.tintColor
|
||||||
|
|
||||||
matchLabel.text = text(for: cap.match)
|
matchLabel.text = cap.matchDescription(match: match)
|
||||||
nameLabel.text = cap.name
|
nameLabel.text = cap.name
|
||||||
countLabel.text = "\(cap.id) (\(cap.count) image" + (cap.count > 1 ? "s)" : ")")
|
countLabel.text = cap.subtitle
|
||||||
}
|
}
|
||||||
|
|
||||||
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])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -10,8 +10,10 @@ import UIKit
|
|||||||
|
|
||||||
class GridViewController: UIViewController {
|
class GridViewController: UIViewController {
|
||||||
|
|
||||||
|
/// The number of caps horizontally.
|
||||||
private let columns = 40
|
private let columns = 40
|
||||||
|
|
||||||
|
/// The number of hroizontal pixels for each cap.
|
||||||
static let len: CGFloat = 60
|
static let len: CGFloat = 60
|
||||||
|
|
||||||
private lazy var rowHeight = GridViewController.len * 0.866
|
private lazy var rowHeight = GridViewController.len * 0.866
|
||||||
@ -24,6 +26,13 @@ class GridViewController: UIViewController {
|
|||||||
|
|
||||||
@IBOutlet weak var scrollView: UIScrollView!
|
@IBOutlet weak var scrollView: UIScrollView!
|
||||||
|
|
||||||
|
/// A dictionary of the caps for the tiles
|
||||||
|
private var tiles = [Cap]()
|
||||||
|
|
||||||
|
private var installedTiles = [Int : RoundedImageView]()
|
||||||
|
|
||||||
|
private var changedTiles = Set<Int>()
|
||||||
|
|
||||||
private var selectedTile: Int? = nil
|
private var selectedTile: Int? = nil
|
||||||
|
|
||||||
private weak var selectionView: RoundedButton!
|
private weak var selectionView: RoundedButton!
|
||||||
@ -38,13 +47,21 @@ class GridViewController: UIViewController {
|
|||||||
|
|
||||||
private var isShowingColors = false
|
private var isShowingColors = false
|
||||||
|
|
||||||
|
private var capCount = 0
|
||||||
|
|
||||||
@IBAction func toggleAverageColor(_ sender: Any) {
|
@IBAction func toggleAverageColor(_ sender: Any) {
|
||||||
isShowingColors = !isShowingColors
|
isShowingColors = !isShowingColors
|
||||||
for (tile, view) in installedTiles {
|
for (tile, view) in installedTiles {
|
||||||
if isShowingColors {
|
if isShowingColors {
|
||||||
view.image = nil
|
view.image = nil
|
||||||
} else {
|
} else {
|
||||||
view.image = Cap.tileImage(tile: tile)
|
if let image = tiles[tile].thumbnail {
|
||||||
|
view.image = image
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tiles[tile].downloadMainImage() { image in
|
||||||
|
view.image = image
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -52,12 +69,15 @@ class GridViewController: UIViewController {
|
|||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
app.database.add(listener: self)
|
||||||
|
capCount = app.database.capCount
|
||||||
|
tiles = app.database.caps.sorted { $0.tile < $1.tile }
|
||||||
|
|
||||||
let width = CGFloat(columns) * GridViewController.len + GridViewController.len / 2
|
let width = CGFloat(columns) * GridViewController.len + GridViewController.len / 2
|
||||||
let height = (CGFloat(Cap.totalCapCount) / CGFloat(columns)).rounded(.up) * rowHeight + margin
|
let height = (CGFloat(capCount) / CGFloat(columns)).rounded(.up) * rowHeight + margin
|
||||||
canvasSize = CGSize(width: width, height: height)
|
canvasSize = CGSize(width: width, height: height)
|
||||||
myView = UIView(frame: CGRect(origin: .zero, size: canvasSize))
|
myView = UIView(frame: CGRect(origin: .zero, size: canvasSize))
|
||||||
|
|
||||||
|
|
||||||
scrollView.addSubview(myView)
|
scrollView.addSubview(myView)
|
||||||
scrollView.contentSize = canvasSize
|
scrollView.contentSize = canvasSize
|
||||||
scrollView.delegate = self
|
scrollView.delegate = self
|
||||||
@ -83,9 +103,35 @@ class GridViewController: UIViewController {
|
|||||||
override func viewWillDisappear(_ animated: Bool) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
super.viewWillDisappear(animated)
|
super.viewWillDisappear(animated)
|
||||||
|
|
||||||
Cap.save()
|
saveChangedTiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Tiles
|
||||||
|
|
||||||
|
private func tileColor(tile: Int) -> UIColor {
|
||||||
|
return tiles[tile].color
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveChangedTiles() {
|
||||||
|
for tile in changedTiles {
|
||||||
|
let cap = tiles[tile]
|
||||||
|
app.database.update(tile: tile, for: cap.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Switch two tiles.
|
||||||
|
*/
|
||||||
|
private func switchTiles(_ lhs: Int, _ rhs: Int) -> Bool {
|
||||||
|
let temp = tiles[rhs]
|
||||||
|
tiles[rhs] = tiles[lhs]
|
||||||
|
tiles[lhs] = temp
|
||||||
|
changedTiles.insert(lhs)
|
||||||
|
changedTiles.insert(rhs)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private func setZoomRange() {
|
private func setZoomRange() {
|
||||||
let size = scrollView.frame.size
|
let size = scrollView.frame.size
|
||||||
let a = size.width / canvasSize.width
|
let a = size.width / canvasSize.width
|
||||||
@ -106,11 +152,16 @@ class GridViewController: UIViewController {
|
|||||||
guard s > margin else {
|
guard s > margin else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let column: CGFloat
|
let column: Int
|
||||||
if row.isEven {
|
if row.isEven {
|
||||||
column = loc.x / GridViewController.len
|
column = Int(loc.x / GridViewController.len)
|
||||||
|
// Abort, if user tapped outside of the grid
|
||||||
|
if column >= columns {
|
||||||
|
clearTileSelection()
|
||||||
|
return
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
column = (loc.x - GridViewController.len / 2) / GridViewController.len
|
column = Int((loc.x - GridViewController.len / 2) / GridViewController.len)
|
||||||
}
|
}
|
||||||
handleTileTapped(tile: row * columns + Int(column))
|
handleTileTapped(tile: row * columns + Int(column))
|
||||||
}
|
}
|
||||||
@ -123,8 +174,6 @@ class GridViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var installedTiles = [Int : RoundedImageView]()
|
|
||||||
|
|
||||||
private func showSelection(tile: Int) {
|
private func showSelection(tile: Int) {
|
||||||
clearTileSelection()
|
clearTileSelection()
|
||||||
|
|
||||||
@ -144,13 +193,23 @@ class GridViewController: UIViewController {
|
|||||||
private func makeTile(_ tile: Int) {
|
private func makeTile(_ tile: Int) {
|
||||||
let view = RoundedImageView(frame: frame(for: tile))
|
let view = RoundedImageView(frame: frame(for: tile))
|
||||||
myView.addSubview(view)
|
myView.addSubview(view)
|
||||||
view.backgroundColor = Cap.tileColor(tile: tile)
|
view.backgroundColor = tileColor(tile: tile)
|
||||||
|
defer {
|
||||||
|
installedTiles[tile] = view
|
||||||
|
}
|
||||||
// Only set image if images are shown
|
// Only set image if images are shown
|
||||||
if !isShowingColors {
|
guard !isShowingColors else {
|
||||||
view.image = Cap.tileImage(tile: tile)
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
if let image = tiles[tile].thumbnail {
|
||||||
|
view.image = image
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
installedTiles[tile] = view
|
tiles[tile].downloadMainImage() { image in
|
||||||
|
view.image = image
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func frame(for tile: Int) -> CGRect {
|
private func frame(for tile: Int) -> CGRect {
|
||||||
@ -166,13 +225,18 @@ class GridViewController: UIViewController {
|
|||||||
clearTileSelection()
|
clearTileSelection()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
Cap.switchTiles(oldTile, newTile)
|
guard switchTiles(oldTile, newTile) else {
|
||||||
|
clearTileSelection()
|
||||||
|
return
|
||||||
|
}
|
||||||
// Switch cap colors
|
// Switch cap colors
|
||||||
installedTiles[oldTile]?.backgroundColor = Cap.tileColor(tile: oldTile)
|
let temp = installedTiles[oldTile]?.backgroundColor
|
||||||
installedTiles[newTile]?.backgroundColor = Cap.tileColor(tile: newTile)
|
installedTiles[oldTile]?.backgroundColor = installedTiles[newTile]?.backgroundColor
|
||||||
|
installedTiles[newTile]?.backgroundColor = temp
|
||||||
if !isShowingColors {
|
if !isShowingColors {
|
||||||
installedTiles[oldTile]?.image = Cap.tileImage(tile: oldTile)
|
let temp = installedTiles[oldTile]?.image
|
||||||
installedTiles[newTile]?.image = Cap.tileImage(tile: newTile)
|
installedTiles[oldTile]?.image = installedTiles[newTile]?.image
|
||||||
|
installedTiles[newTile]?.image = temp
|
||||||
}
|
}
|
||||||
clearTileSelection()
|
clearTileSelection()
|
||||||
|
|
||||||
@ -187,32 +251,46 @@ class GridViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func showTiles(in rect: CGRect) {
|
private func showTiles(in rect: CGRect) {
|
||||||
for i in 0..<Cap.totalCapCount {
|
for tile in 0..<capCount {
|
||||||
if tileIsVisible(tile: i, in: rect) {
|
refresh(tile: tile, inVisibleRect: rect)
|
||||||
if installedTiles[i] != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
makeTile(i)
|
|
||||||
} else if let tile = installedTiles[i] {
|
|
||||||
tile.removeFromSuperview()
|
|
||||||
installedTiles[i] = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateTiles() {
|
private func refresh(tile: Int, inVisibleRect rect: CGRect) {
|
||||||
guard #available(iOS 12.0, *) else {
|
if tileIsVisible(tile: tile, in: rect) {
|
||||||
|
show(tile: tile)
|
||||||
|
} else if let installed = installedTiles[tile] {
|
||||||
|
installed.removeFromSuperview()
|
||||||
|
installedTiles[tile] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func show(tile: Int) {
|
||||||
|
guard installedTiles[tile] == nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
makeTile(tile)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func remove(tile: Int) {
|
||||||
|
installedTiles[tile]?.removeFromSuperview()
|
||||||
|
installedTiles[tile] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private var visibleRect: CGRect {
|
||||||
let scale = scrollView.zoomScale
|
let scale = scrollView.zoomScale
|
||||||
let offset = scrollView.contentOffset
|
let offset = scrollView.contentOffset
|
||||||
let size = scrollView.visibleSize
|
let size = scrollView.visibleSize
|
||||||
|
|
||||||
let scaledOrigin = CGPoint(x: offset.x / scale, y: offset.y / scale)
|
let scaledOrigin = CGPoint(x: offset.x / scale, y: offset.y / scale)
|
||||||
let scaledSize = CGSize(width: size.width / scale, height: size.height / scale)
|
let scaledSize = CGSize(width: size.width / scale, height: size.height / scale)
|
||||||
let rect = CGRect(origin: scaledOrigin, size: scaledSize)
|
return CGRect(origin: scaledOrigin, size: scaledSize)
|
||||||
|
}
|
||||||
|
|
||||||
showTiles(in: rect)
|
private func updateTiles() {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.showTiles(in: self.visibleRect)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,8 +312,31 @@ private extension Int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension GridViewController: Logger {
|
extension GridViewController: Logger { }
|
||||||
|
|
||||||
static let logToken: String = "[Grid]"
|
extension GridViewController: DatabaseDelegate {
|
||||||
|
|
||||||
|
func database(didChangeCap id: Int) {
|
||||||
|
guard let view = installedTiles[id] else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let cap = app.database.cap(for: id) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tiles[cap.tile] = cap
|
||||||
|
view.backgroundColor = cap.color
|
||||||
|
// Only set image if images are shown
|
||||||
|
if !isShowingColors {
|
||||||
|
view.image = cap.image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func database(didAddCap cap: Cap) {
|
||||||
|
tiles.append(cap)
|
||||||
|
refresh(tile: cap.tile, inVisibleRect: visibleRect)
|
||||||
|
}
|
||||||
|
|
||||||
|
func databaseRequiresFullRefresh() {
|
||||||
|
updateTiles()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import SwiftyDropbox
|
|
||||||
|
|
||||||
class ImageSelector: UIViewController {
|
class ImageSelector: UIViewController {
|
||||||
|
|
||||||
@ -28,7 +27,11 @@ class ImageSelector: UIViewController {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - CollectionView
|
// MARK: - Variables
|
||||||
|
|
||||||
|
private var titleLabel: UILabel!
|
||||||
|
|
||||||
|
private var subtitleLabel: UILabel!
|
||||||
|
|
||||||
private var images = [UIImage?]()
|
private var images = [UIImage?]()
|
||||||
|
|
||||||
@ -36,6 +39,14 @@ class ImageSelector: UIViewController {
|
|||||||
|
|
||||||
@IBOutlet weak var collection: UICollectionView!
|
@IBOutlet weak var collection: UICollectionView!
|
||||||
|
|
||||||
|
private var titleText: String {
|
||||||
|
"Cap \(cap.id) (\(cap.count) images)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var subtitleText: String {
|
||||||
|
cap.name
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Life cycle
|
// MARK: - Life cycle
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
@ -43,6 +54,7 @@ class ImageSelector: UIViewController {
|
|||||||
|
|
||||||
collection.dataSource = self
|
collection.dataSource = self
|
||||||
collection.delegate = self
|
collection.delegate = self
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
@ -51,17 +63,45 @@ class ImageSelector: UIViewController {
|
|||||||
downloadImages()
|
downloadImages()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func didMove(toParent parent: UIViewController?) {
|
||||||
|
super.didMove(toParent: parent)
|
||||||
|
|
||||||
|
guard parent != nil && self.navigationItem.titleView == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
initNavigationItemTitleView()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func initNavigationItemTitleView() {
|
||||||
|
self.titleLabel = UILabel()
|
||||||
|
titleLabel.text = titleText
|
||||||
|
titleLabel.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
titleLabel.textColor = .label
|
||||||
|
|
||||||
|
self.subtitleLabel = UILabel()
|
||||||
|
subtitleLabel.text = subtitleText
|
||||||
|
subtitleLabel.font = .preferredFont(forTextStyle: .footnote)
|
||||||
|
subtitleLabel.textColor = .secondaryLabel
|
||||||
|
|
||||||
|
let stackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
|
||||||
|
stackView.distribution = .equalCentering
|
||||||
|
stackView.alignment = .center
|
||||||
|
stackView.axis = .vertical
|
||||||
|
|
||||||
|
self.navigationItem.titleView = stackView
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Image download
|
// MARK: Image download
|
||||||
|
|
||||||
private func downloadImages() {
|
private func downloadImages() {
|
||||||
images = [UIImage?](repeating: nil, count: cap.count)
|
images = [UIImage?](repeating: nil, count: cap.count)
|
||||||
event("\(cap.count) images for cap \(cap.id)")
|
log("\(cap.count) images for cap \(cap.id)")
|
||||||
if let image = cap.image {
|
if let image = cap.image {
|
||||||
self.images[0] = image
|
self.images[0] = image
|
||||||
self.collection.reloadItems(at: [IndexPath(row: 0, section: 0)])
|
self.collection.reloadItems(at: [IndexPath(row: 0, section: 0)])
|
||||||
} else {
|
} else {
|
||||||
cap.downloadImage { mainImage in
|
cap.downloadMainImage { image in
|
||||||
self.images[0] = mainImage
|
self.images[0] = image
|
||||||
self.collection.reloadItems(at: [IndexPath(row: 0, section: 0)])
|
self.collection.reloadItems(at: [IndexPath(row: 0, section: 0)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -69,7 +109,10 @@ class ImageSelector: UIViewController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for number in 1..<cap.count {
|
for number in 1..<cap.count {
|
||||||
cap.downloadImage(number) { image in
|
_ = cap.downloadImage(number) { image in
|
||||||
|
guard let image = image else {
|
||||||
|
return
|
||||||
|
}
|
||||||
self.images[number] = image
|
self.images[number] = image
|
||||||
self.collection.reloadItems(at: [IndexPath(row: number, section: 0)])
|
self.collection.reloadItems(at: [IndexPath(row: number, section: 0)])
|
||||||
}
|
}
|
||||||
@ -79,11 +122,7 @@ class ImageSelector: UIViewController {
|
|||||||
// MARK: Select
|
// MARK: Select
|
||||||
|
|
||||||
private func selectedImage(nr: Int) {
|
private func selectedImage(nr: Int) {
|
||||||
guard let image = images[nr] else {
|
app.database.setMainImage(of: cap.id, to: nr)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cap.setMainImage(to: nr, image: image)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +143,7 @@ extension ImageSelector: UICollectionViewDataSource {
|
|||||||
withReuseIdentifier: "Image",
|
withReuseIdentifier: "Image",
|
||||||
for: indexPath) as! ImageCell
|
for: indexPath) as! ImageCell
|
||||||
|
|
||||||
cell.capView.image = images[indexPath.row]
|
cell.capView.image = images[indexPath.row] ?? UIImage(named: "launch")
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,9 +177,6 @@ extension ImageSelector : UICollectionViewDelegateFlowLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ImageSelector: Logger {
|
extension ImageSelector: Logger { }
|
||||||
|
|
||||||
static let logToken = "[ImageSelector]"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
185
CapCollector/Presentation/SearchAndDisplayAccessory.swift
Normal file
185
CapCollector/Presentation/SearchAndDisplayAccessory.swift
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
//
|
||||||
|
// SearchAndDisplayAccessory.swift
|
||||||
|
// CapCollector
|
||||||
|
//
|
||||||
|
// Created by Christoph on 09.10.19.
|
||||||
|
// Copyright © 2019 CH. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class PassthroughView: UIView {
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
let view = super.hitTest(point, with: event)
|
||||||
|
return view == self ? nil : view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol CapAccessoryDelegate: class {
|
||||||
|
|
||||||
|
func capSearchWasDismissed()
|
||||||
|
|
||||||
|
func capSearch(didChange text: String)
|
||||||
|
|
||||||
|
func capAccessoryDidDiscardImage()
|
||||||
|
|
||||||
|
func capAccessory(shouldSave image: UIImage)
|
||||||
|
|
||||||
|
func capAccessoryCameraButtonPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchAndDisplayAccessory: PassthroughView {
|
||||||
|
|
||||||
|
// MARK: - Outlets
|
||||||
|
|
||||||
|
@IBOutlet weak var newImageView: PassthroughView!
|
||||||
|
|
||||||
|
@IBOutlet weak var capImage: RoundedImageView!
|
||||||
|
|
||||||
|
@IBOutlet weak var saveButton: UIButton!
|
||||||
|
|
||||||
|
@IBOutlet weak var deleteButton: UIButton!
|
||||||
|
|
||||||
|
@IBOutlet weak var cameraButton: UIButton!
|
||||||
|
|
||||||
|
@IBOutlet weak var searchBar: UISearchBar!
|
||||||
|
|
||||||
|
// MARK: - Actions
|
||||||
|
|
||||||
|
@IBAction func cameraButtonPressed() {
|
||||||
|
delegate?.capAccessoryCameraButtonPressed()
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func saveButtonPressed() {
|
||||||
|
if let image = capImage.image {
|
||||||
|
delegate?.capAccessory(shouldSave: image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@IBAction func cancelButtonPressed() {
|
||||||
|
discardImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Variables
|
||||||
|
|
||||||
|
var view: UIView?
|
||||||
|
|
||||||
|
weak var blurView: UIVisualEffectView?
|
||||||
|
|
||||||
|
weak var currentBlurContraint: NSLayoutConstraint?
|
||||||
|
|
||||||
|
weak var delegate: CapAccessoryDelegate?
|
||||||
|
|
||||||
|
var currentImage: UIImage? {
|
||||||
|
capImage.image
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Setup
|
||||||
|
|
||||||
|
convenience init(width: CGFloat) {
|
||||||
|
let frame = CGRect(origin: .zero, size: CGSize(width: width, height: 145))
|
||||||
|
self.init(frame: frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setup() {
|
||||||
|
view = fromNib()
|
||||||
|
view!.frame = bounds
|
||||||
|
//view!.autoresizingMask = .flexibleHeight
|
||||||
|
addSubview(view!)
|
||||||
|
|
||||||
|
let blur = UIBlurEffect(style: .systemThinMaterial)
|
||||||
|
let blurView = UIVisualEffectView(effect: blur)
|
||||||
|
self.blurView = blurView
|
||||||
|
blurView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
blurView.isUserInteractionEnabled = false
|
||||||
|
insertSubview(blurView, at: 0)
|
||||||
|
|
||||||
|
let t = searchBar.topAnchor.constraint(equalTo: blurView.topAnchor)
|
||||||
|
let b = searchBar.bottomAnchor.constraint(equalTo: blurView.bottomAnchor)
|
||||||
|
let l = leadingAnchor.constraint(equalTo: blurView.leadingAnchor)
|
||||||
|
let r = trailingAnchor.constraint(equalTo: blurView.trailingAnchor)
|
||||||
|
addConstraints([t, b, l, r])
|
||||||
|
|
||||||
|
currentBlurContraint = t
|
||||||
|
|
||||||
|
self.newImageView.alpha = 0
|
||||||
|
self.newImageView.isHidden = true
|
||||||
|
|
||||||
|
searchBar.text = nil
|
||||||
|
searchBar.setShowsCancelButton(false, animated: false)
|
||||||
|
searchBar.delegate = self
|
||||||
|
|
||||||
|
cameraButton.setImage(UIImage.templateImage(named: "camera_square"), for: .normal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Search bar
|
||||||
|
|
||||||
|
func dismissAndClearSearchBar() {
|
||||||
|
searchBar.resignFirstResponder()
|
||||||
|
searchBar.text = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Cap image
|
||||||
|
|
||||||
|
func showImageView(with image: UIImage) {
|
||||||
|
capImage.image = image
|
||||||
|
showImageView()
|
||||||
|
}
|
||||||
|
|
||||||
|
func discardImage() {
|
||||||
|
dismissAndClearSearchBar()
|
||||||
|
hideImageView()
|
||||||
|
delegate?.capAccessoryDidDiscardImage()
|
||||||
|
}
|
||||||
|
|
||||||
|
func hideImageView() {
|
||||||
|
currentBlurContraint?.isActive = false
|
||||||
|
let t = searchBar.topAnchor.constraint(equalTo: blurView!.topAnchor)
|
||||||
|
addConstraint(t)
|
||||||
|
currentBlurContraint = t
|
||||||
|
|
||||||
|
self.newImageView.alpha = 0
|
||||||
|
self.newImageView.isHidden = true
|
||||||
|
self.capImage.image = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showImageView() {
|
||||||
|
currentBlurContraint?.isActive = false
|
||||||
|
let t = blurView!.topAnchor.constraint(equalTo: saveButton.topAnchor, constant: -8)
|
||||||
|
addConstraint(t)
|
||||||
|
currentBlurContraint = t
|
||||||
|
|
||||||
|
self.newImageView.isHidden = false
|
||||||
|
self.newImageView.alpha = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UISearchBarDelegate
|
||||||
|
|
||||||
|
extension SearchAndDisplayAccessory: UISearchBarDelegate {
|
||||||
|
|
||||||
|
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
|
||||||
|
searchBar.resignFirstResponder()
|
||||||
|
searchBar.text = nil
|
||||||
|
delegate?.capSearchWasDismissed()
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
|
||||||
|
searchBar.resignFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
|
||||||
|
delegate?.capSearch(didChange: searchText)
|
||||||
|
}
|
||||||
|
}
|
117
CapCollector/Presentation/SearchAndDisplayAccessory.xib
Normal file
117
CapCollector/Presentation/SearchAndDisplayAccessory.xib
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16086"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SearchAndDisplayAccessory" customModule="CapCollector" customModuleProvider="target">
|
||||||
|
<connections>
|
||||||
|
<outlet property="cameraButton" destination="bIA-eq-Tn5" id="r0F-0j-Ve9"/>
|
||||||
|
<outlet property="capImage" destination="vQm-nH-J8o" id="bQK-Vu-Z1U"/>
|
||||||
|
<outlet property="deleteButton" destination="qhB-Sd-K8H" id="oym-9o-1m3"/>
|
||||||
|
<outlet property="newImageView" destination="0wK-yR-rO9" id="Gi3-I6-Xv9"/>
|
||||||
|
<outlet property="saveButton" destination="dt5-LD-28a" id="IYT-eN-3lb"/>
|
||||||
|
<outlet property="searchBar" destination="bCh-7y-t0w" id="8Tt-4h-Fkg"/>
|
||||||
|
</connections>
|
||||||
|
</placeholder>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="PassthroughView" customModule="CapCollector" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="414" height="145"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<searchBar contentMode="redraw" searchBarStyle="minimal" placeholder="Search caps" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bCh-7y-t0w">
|
||||||
|
<rect key="frame" x="0.0" y="89" width="358" height="56"/>
|
||||||
|
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardAppearance="alert" returnKeyType="search" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no"/>
|
||||||
|
<scopeButtonTitles>
|
||||||
|
<string>Title</string>
|
||||||
|
<string>Title</string>
|
||||||
|
</scopeButtonTitles>
|
||||||
|
</searchBar>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="bIA-eq-Tn5">
|
||||||
|
<rect key="frame" x="358" y="89" width="56" height="56"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" secondItem="bIA-eq-Tn5" secondAttribute="height" multiplier="1:1" id="O09-ww-bHE"/>
|
||||||
|
<constraint firstAttribute="height" constant="56" id="Y3N-l5-d94"/>
|
||||||
|
</constraints>
|
||||||
|
<inset key="imageEdgeInsets" minX="5" minY="5" maxX="5" maxY="5"/>
|
||||||
|
<state key="normal" image="camera_square"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="cameraButtonPressed" destination="-1" eventType="touchUpInside" id="ooo-b8-Atj"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0wK-yR-rO9">
|
||||||
|
<rect key="frame" x="0.0" y="-1" width="414" height="90"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="vQm-nH-J8o" customClass="RoundedImageView" customModule="CapCollector" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="2" y="2" width="86" height="86"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" secondItem="vQm-nH-J8o" secondAttribute="height" multiplier="1:1" id="WHb-tV-k4S"/>
|
||||||
|
</constraints>
|
||||||
|
<userDefinedRuntimeAttributes>
|
||||||
|
<userDefinedRuntimeAttribute type="color" keyPath="borderColor">
|
||||||
|
<color key="value" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</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="dt5-LD-28a">
|
||||||
|
<rect key="frame" x="96" y="60" width="151" height="30"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="30" id="Boy-dJ-2BJ"/>
|
||||||
|
</constraints>
|
||||||
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
|
<state key="normal" title="Save"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="saveButtonPressed" destination="-1" eventType="touchUpInside" id="O49-6L-mNY"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qhB-Sd-K8H">
|
||||||
|
<rect key="frame" x="255" y="58.5" width="151" height="33"/>
|
||||||
|
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||||
|
<state key="normal" title="Delete"/>
|
||||||
|
<connections>
|
||||||
|
<action selector="cancelButtonPressed" destination="-1" eventType="touchUpInside" id="4Sm-nV-aOd"/>
|
||||||
|
</connections>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="vQm-nH-J8o" firstAttribute="leading" secondItem="0wK-yR-rO9" secondAttribute="leading" constant="2" id="6q2-Lc-eIF"/>
|
||||||
|
<constraint firstItem="dt5-LD-28a" firstAttribute="leading" secondItem="vQm-nH-J8o" secondAttribute="trailing" constant="8" id="OUT-X8-cxn"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="vQm-nH-J8o" secondAttribute="bottom" constant="2" id="P8h-BA-8Yc"/>
|
||||||
|
<constraint firstAttribute="height" constant="90" id="Rsk-qm-0WZ"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="qhB-Sd-K8H" secondAttribute="trailing" constant="8" id="fzn-7b-WrO"/>
|
||||||
|
<constraint firstItem="vQm-nH-J8o" firstAttribute="top" secondItem="0wK-yR-rO9" secondAttribute="top" constant="2" id="h1D-Ut-lnp"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="dt5-LD-28a" secondAttribute="bottom" id="wFi-0u-ian"/>
|
||||||
|
<constraint firstItem="qhB-Sd-K8H" firstAttribute="leading" secondItem="dt5-LD-28a" secondAttribute="trailing" constant="8" id="y1c-g7-5zg"/>
|
||||||
|
<constraint firstItem="qhB-Sd-K8H" firstAttribute="centerY" secondItem="dt5-LD-28a" secondAttribute="centerY" id="yhy-DU-ReF"/>
|
||||||
|
<constraint firstItem="qhB-Sd-K8H" firstAttribute="width" secondItem="dt5-LD-28a" secondAttribute="width" id="zpv-6V-gXr"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="bIA-eq-Tn5" secondAttribute="bottom" id="Geo-F0-pdI"/>
|
||||||
|
<constraint firstItem="bCh-7y-t0w" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="M9M-He-vxM"/>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="top" secondItem="0wK-yR-rO9" secondAttribute="top" constant="1" id="Uyp-J7-bKr"/>
|
||||||
|
<constraint firstItem="bIA-eq-Tn5" firstAttribute="leading" secondItem="bCh-7y-t0w" secondAttribute="trailing" id="i7w-LO-9lv"/>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="0wK-yR-rO9" secondAttribute="trailing" id="iWS-vI-RAT"/>
|
||||||
|
<constraint firstItem="bCh-7y-t0w" firstAttribute="top" secondItem="0wK-yR-rO9" secondAttribute="bottom" id="iab-RT-0nN"/>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="bIA-eq-Tn5" secondAttribute="trailing" id="kQV-mX-EoD"/>
|
||||||
|
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="bCh-7y-t0w" secondAttribute="bottom" id="r8c-kO-KT5"/>
|
||||||
|
<constraint firstItem="0wK-yR-rO9" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="rd3-iN-jtD"/>
|
||||||
|
</constraints>
|
||||||
|
<nil key="simulatedTopBarMetrics"/>
|
||||||
|
<nil key="simulatedBottomBarMetrics"/>
|
||||||
|
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||||
|
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||||
|
<point key="canvasLocation" x="97.5" y="105.625"/>
|
||||||
|
</view>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<image name="camera_square" width="220" height="220"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
@ -1,272 +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 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 isUpdatingCounts = false
|
|
||||||
|
|
||||||
private var isUploadingNameFile = false
|
|
||||||
|
|
||||||
private var isUpdatingThumbnails = false
|
|
||||||
|
|
||||||
private var isUpdatingColors = 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)
|
|
||||||
setClassifierChoice(Persistence.useMobileNet)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateThumbnails() {
|
|
||||||
isUpdatingThumbnails = true
|
|
||||||
for cap in Cap.all.values {
|
|
||||||
cap.makeThumbnail()
|
|
||||||
}
|
|
||||||
isUpdatingThumbnails = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateColors() {
|
|
||||||
isUpdatingColors = true
|
|
||||||
Cap.shouldSave = false
|
|
||||||
for cap in Cap.all.values {
|
|
||||||
cap.makeAverageColor()
|
|
||||||
}
|
|
||||||
Cap.shouldSave = true
|
|
||||||
isUpdatingColors = 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
|
|
||||||
databaseUpdatesLabel.text = "\(capCount) new caps and \(imageCount) new images"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateDropboxStatus() {
|
|
||||||
dropboxAccountLabel.text = DropboxController.shared.isEnabled ? "Sign out" : "Sign in"
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setClassifierChoice(_ useMobileNet: Bool) {
|
|
||||||
tableView.cellForRow(at: IndexPath(row: 0, section: 0))?.accessoryType = useMobileNet ? .checkmark : .none
|
|
||||||
}
|
|
||||||
|
|
||||||
private func toggleClassifier() {
|
|
||||||
let newValue = !Persistence.useMobileNet
|
|
||||||
Persistence.useMobileNet = newValue
|
|
||||||
setClassifierChoice(newValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
|
|
||||||
switch indexPath.section {
|
|
||||||
case 0: // Choose models
|
|
||||||
return true
|
|
||||||
case 1: // Mosaic
|
|
||||||
return true
|
|
||||||
case 2: // Database
|
|
||||||
return indexPath.row == 2 && !isUploadingNameFile
|
|
||||||
case 3: // Refresh
|
|
||||||
switch indexPath.row {
|
|
||||||
case 0:
|
|
||||||
return !isUpdatingCounts
|
|
||||||
case 1:
|
|
||||||
return !isUpdatingThumbnails
|
|
||||||
case 2:
|
|
||||||
return !isUpdatingColors
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case 4: // Dropbox account
|
|
||||||
return true
|
|
||||||
case 5: // Log file
|
|
||||||
return true
|
|
||||||
default: return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
|
||||||
switch indexPath.section {
|
|
||||||
case 0: // Choose models
|
|
||||||
return indexPath
|
|
||||||
case 1: // Mosaic
|
|
||||||
return indexPath
|
|
||||||
case 2: // Database
|
|
||||||
return (indexPath.row == 2 && !isUploadingNameFile) ? indexPath : nil
|
|
||||||
case 3: // Refresh count
|
|
||||||
switch indexPath.row {
|
|
||||||
case 0:
|
|
||||||
return isUpdatingCounts ? nil : indexPath
|
|
||||||
case 1:
|
|
||||||
return isUpdatingThumbnails ? nil : indexPath
|
|
||||||
case 2:
|
|
||||||
return isUpdatingColors ? nil : indexPath
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
case 4: // Dropbox account
|
|
||||||
return indexPath
|
|
||||||
case 5: // Log file
|
|
||||||
return indexPath
|
|
||||||
default: return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
||||||
tableView.deselectRow(at: indexPath, animated: true)
|
|
||||||
switch indexPath.section {
|
|
||||||
case 0: // Choose models
|
|
||||||
toggleClassifier()
|
|
||||||
case 2: // Upload
|
|
||||||
if indexPath.row == 2 && !isUploadingNameFile {
|
|
||||||
uploadNameFile()
|
|
||||||
}
|
|
||||||
case 3: // Refresh count
|
|
||||||
switch indexPath.row {
|
|
||||||
case 0:
|
|
||||||
updateCounts()
|
|
||||||
case 1:
|
|
||||||
updateThumbnails()
|
|
||||||
case 2:
|
|
||||||
updateColors()
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
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() {
|
|
||||||
event("Uploading name file")
|
|
||||||
isUploadingNameFile = true
|
|
||||||
Cap.saveAndUpload() { _ in
|
|
||||||
self.isUploadingNameFile = false
|
|
||||||
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 else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch id {
|
|
||||||
case "showMosaic":
|
|
||||||
(navigationController as! NavigationController).allowLandscape = true
|
|
||||||
case "showLog":
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -15,7 +15,9 @@ enum SortCriteria: Int {
|
|||||||
case match = 3
|
case match = 3
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol SortControllerDelegate {
|
protocol SortControllerDelegate: class {
|
||||||
|
|
||||||
|
var sortControllerShouldIncludeMatchOption: Bool { get }
|
||||||
|
|
||||||
func sortController(didSelect sortType: SortCriteria, ascending: Bool)
|
func sortController(didSelect sortType: SortCriteria, ascending: Bool)
|
||||||
}
|
}
|
||||||
@ -26,32 +28,19 @@ class SortController: UITableViewController {
|
|||||||
|
|
||||||
var ascending: Bool = true
|
var ascending: Bool = true
|
||||||
|
|
||||||
var delegate: SortControllerDelegate?
|
private var includeMatches: Bool {
|
||||||
|
delegate?.sortControllerShouldIncludeMatchOption ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
weak var delegate: SortControllerDelegate?
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
let height = Cap.hasMatches ? 310 : 270
|
let height = includeMatches ? 298 : 258
|
||||||
preferredContentSize = CGSize(width: 200, height: height)
|
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) {
|
private func giveFeedback(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
|
||||||
let generator = UIImpactFeedbackGenerator(style: style)
|
let generator = UIImpactFeedbackGenerator(style: style)
|
||||||
generator.impactOccurred()
|
generator.impactOccurred()
|
||||||
@ -63,7 +52,7 @@ class SortController: UITableViewController {
|
|||||||
tableView.deselectRow(at: indexPath, animated: true)
|
tableView.deselectRow(at: indexPath, animated: true)
|
||||||
guard indexPath.section == 1 else {
|
guard indexPath.section == 1 else {
|
||||||
ascending = !ascending
|
ascending = !ascending
|
||||||
setCell()
|
tableView.reloadData()
|
||||||
delegate?.sortController(didSelect: selected, ascending: ascending)
|
delegate?.sortController(didSelect: selected, ascending: ascending)
|
||||||
giveFeedback(.light)
|
giveFeedback(.light)
|
||||||
return
|
return
|
||||||
@ -76,17 +65,16 @@ class SortController: UITableViewController {
|
|||||||
|
|
||||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||||
guard indexPath.row == 3 else { return indexPath }
|
guard indexPath.row == 3 else { return indexPath }
|
||||||
return Cap.hasMatches ? indexPath : nil
|
return includeMatches ? indexPath : nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
|
||||||
// MARK: - Navigation
|
switch indexPath.section {
|
||||||
|
case 0:
|
||||||
// In a storyboard-based application, you will often want to do a little preparation before navigation
|
cell.accessoryType = ascending ? .checkmark : .none
|
||||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
default:
|
||||||
// Get the new view controller using segue.destination.
|
cell.accessoryType = indexPath.row == selected.rawValue ? .checkmark : .none
|
||||||
// Pass the selected object to the new view controller.
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,89 +0,0 @@
|
|||||||
//
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user