Start version 2
@ -3,227 +3,196 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 52;
|
objectVersion = 55;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
5904C33A2199C9FA0046A573 /* SortController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5904C3392199C9FA0046A573 /* SortController.swift */; };
|
E25AAC7C283D855D006E9E7F /* CapsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC7B283D855D006E9E7F /* CapsApp.swift */; };
|
||||||
5904C33C2199D0260046A573 /* AlwaysShowPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */; };
|
E25AAC7E283D855D006E9E7F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC7D283D855D006E9E7F /* ContentView.swift */; };
|
||||||
59158B1621E37B0200D90CB0 /* GridViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59158B1521E37B0200D90CB0 /* GridViewController.swift */; };
|
E25AAC80283D855F006E9E7F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E25AAC7F283D855F006E9E7F /* Assets.xcassets */; };
|
||||||
59158B1821E4C9AC00D90CB0 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59158B1721E4C9AC00D90CB0 /* NavigationController.swift */; };
|
E25AAC83283D855F006E9E7F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E25AAC82283D855F006E9E7F /* Preview Assets.xcassets */; };
|
||||||
591832CE21A2A97E00E5987D /* Cap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591832CD21A2A97E00E5987D /* Cap.swift */; };
|
E25AAC8B283D868D006E9E7F /* Classifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC8A283D868D006E9E7F /* Classifier.swift */; };
|
||||||
591FDD1E234E151600AA379E /* SearchAndDisplayAccessory.xib in Resources */ = {isa = PBXBuildFile; fileRef = 591FDD1D234E151600AA379E /* SearchAndDisplayAccessory.xib */; };
|
E25AAC8D283D86CF006E9E7F /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC8C283D86CF006E9E7F /* Logger.swift */; };
|
||||||
591FDD20234E162000AA379E /* SearchAndDisplayAccessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591FDD1F234E162000AA379E /* SearchAndDisplayAccessory.swift */; };
|
E25AAC90283D871E006E9E7F /* CGImagePropertyOrientation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC8F283D871E006E9E7F /* CGImagePropertyOrientation+Extensions.swift */; };
|
||||||
88A89ECE25AF420F00323B64 /* DispatchGroup+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A89ECD25AF420F00323B64 /* DispatchGroup+Extensions.swift */; };
|
E25AAC92283D8808006E9E7F /* CapData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC91283D8808006E9E7F /* CapData.swift */; };
|
||||||
CE0A501124752A9800A9E753 /* TileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0A501024752A9800A9E753 /* TileImage.swift */; };
|
E25AAC94283D88A4006E9E7F /* Cap.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC93283D88A4006E9E7F /* Cap.swift */; };
|
||||||
CE0A5013247D745200A9E753 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0A5012247D745200A9E753 /* Colors.swift */; };
|
E25AAC96283E14DF006E9E7F /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC95283E14DF006E9E7F /* Database.swift */; };
|
||||||
CE56CECE209D81DE00932C01 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CECD209D81DE00932C01 /* AppDelegate.swift */; };
|
E25AAC9B283E3395006E9E7F /* CapRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC9A283E3395006E9E7F /* CapRowView.swift */; };
|
||||||
CE56CED3209D81DE00932C01 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE56CED1209D81DE00932C01 /* Main.storyboard */; };
|
E27E15E1283E418600F6804A /* CachedAsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = E27E15E0283E418600F6804A /* CachedAsyncImage */; };
|
||||||
CE56CED5209D81E000932C01 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE56CED4209D81E000932C01 /* Assets.xcassets */; };
|
E2EA00C3283E672A00F7B269 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2EA00C2283E672A00F7B269 /* SFSafeSymbols */; };
|
||||||
CE56CED8209D81E000932C01 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE56CED6209D81E000932C01 /* LaunchScreen.storyboard */; };
|
E2EA00C5283EA72000F7B269 /* SortCriteria.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00C4283EA72000F7B269 /* SortCriteria.swift */; };
|
||||||
CE56CEF8209D83B800932C01 /* CapCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE0209D83B200932C01 /* CapCell.swift */; };
|
E2EA00C7283EAA0100F7B269 /* SortSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */; };
|
||||||
CE56CEFE209D83B800932C01 /* RoundedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE6209D83B300932C01 /* RoundedButton.swift */; };
|
E2EA00CA283EACB200F7B269 /* BottomSheet in Frameworks */ = {isa = PBXBuildFile; productRef = E2EA00C9283EACB200F7B269 /* BottomSheet */; };
|
||||||
CE56CEFF209D83B800932C01 /* CameraController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE7209D83B300932C01 /* CameraController.swift */; };
|
E2EA00CC283EB43E00F7B269 /* SortCaseRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */; };
|
||||||
CE56CF02209D83B800932C01 /* RoundedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEA209D83B400932C01 /* RoundedImageView.swift */; };
|
E2EA00CE283EBEB600F7B269 /* SearchField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00CD283EBEB600F7B269 /* SearchField.swift */; };
|
||||||
CE56CF03209D83B800932C01 /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEB209D83B400932C01 /* TableView.swift */; };
|
E2EA00D1283EDD6300F7B269 /* CameraManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00D0283EDD6300F7B269 /* CameraManager.swift */; };
|
||||||
CE56CF04209D83B800932C01 /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEC209D83B400932C01 /* UIViewExtensions.swift */; };
|
E2EA00D3283EDDF700F7B269 /* CameraError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00D2283EDDF700F7B269 /* CameraError.swift */; };
|
||||||
CE56CF05209D83B800932C01 /* ViewControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEED209D83B400932C01 /* ViewControllerExtensions.swift */; };
|
E2EA00D5283EDFA200F7B269 /* FrameManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00D4283EDFA200F7B269 /* FrameManager.swift */; };
|
||||||
CE56CF06209D83B800932C01 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEE209D83B500932C01 /* CameraView.swift */; };
|
E2EA00D9283F5BB900F7B269 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00D8283F5BB900F7B269 /* CameraView.swift */; };
|
||||||
CE56CF07209D83B800932C01 /* ImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEF209D83B500932C01 /* ImageCell.swift */; };
|
E2EA00DB283F5C0600F7B269 /* ContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00DA283F5C0600F7B269 /* ContentViewModel.swift */; };
|
||||||
CE56CF08209D83B800932C01 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF0209D83B500932C01 /* Storage.swift */; };
|
E2EA00DD283F5C6A00F7B269 /* FrameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00DC283F5C6A00F7B269 /* FrameView.swift */; };
|
||||||
CE56CF09209D83B800932C01 /* Classifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF1209D83B500932C01 /* Classifier.swift */; };
|
E2EA00DF283F5CA000F7B269 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00DE283F5CA000F7B269 /* ErrorView.swift */; };
|
||||||
CE56CF0A209D83B800932C01 /* CropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF2209D83B600932C01 /* CropView.swift */; };
|
E2EA00E1283F658E00F7B269 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00E0283F658E00F7B269 /* SettingsView.swift */; };
|
||||||
CE56CF0B209D83B800932C01 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF3209D83B600932C01 /* Logger.swift */; };
|
E2EA00E3283F662800F7B269 /* GridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00E2283F662800F7B269 /* GridView.swift */; };
|
||||||
CE56CF0D209D83B800932C01 /* ImageSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF5209D83B600932C01 /* ImageSelector.swift */; };
|
E2EA00E5283F69DF00F7B269 /* SettingsStatisticRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00E4283F69DF00F7B269 /* SettingsStatisticRow.swift */; };
|
||||||
CE56CF0E209D83B800932C01 /* PhotoCaptureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF6209D83B700932C01 /* PhotoCaptureHandler.swift */; };
|
E2EA00E7283F6D0800F7B269 /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00E6283F6D0800F7B269 /* URL+Extensions.swift */; };
|
||||||
CE56CF0F209D83B800932C01 /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF7209D83B700932C01 /* UIImage+Extensions.swift */; };
|
E2EA00EB284109CC00F7B269 /* CGImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00EA284109CC00F7B269 /* CGImage+Extensions.swift */; };
|
||||||
CE5B7CFC24562673002E5C06 /* Download.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5B7CFB24562673002E5C06 /* Download.swift */; };
|
E2EA00ED2841170100F7B269 /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00EC2841170100F7B269 /* UIImage+Extensions.swift */; };
|
||||||
CE5B7CFE245626D3002E5C06 /* Upload.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5B7CFD245626D3002E5C06 /* Upload.swift */; };
|
E2EA00EF28420AA000F7B269 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00EE28420AA000F7B269 /* Data+Extensions.swift */; };
|
||||||
CE5B7D032458C921002E5C06 /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = CE5B7D022458C921002E5C06 /* Reachability */; };
|
E2EA00F328438E6B00F7B269 /* CapNameEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00F228438E6B00F7B269 /* CapNameEntryView.swift */; };
|
||||||
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 */; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
5904C3392199C9FA0046A573 /* SortController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortController.swift; sourceTree = "<group>"; };
|
E25AAC78283D855D006E9E7F /* Caps.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Caps.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlwaysShowPopup.swift; sourceTree = "<group>"; };
|
E25AAC7B283D855D006E9E7F /* CapsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapsApp.swift; sourceTree = "<group>"; };
|
||||||
59158B1521E37B0200D90CB0 /* GridViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridViewController.swift; sourceTree = "<group>"; };
|
E25AAC7D283D855D006E9E7F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
59158B1721E4C9AC00D90CB0 /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = "<group>"; };
|
E25AAC7F283D855F006E9E7F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
591832CD21A2A97E00E5987D /* Cap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cap.swift; sourceTree = "<group>"; };
|
E25AAC82283D855F006E9E7F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||||
591FDD1D234E151600AA379E /* SearchAndDisplayAccessory.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SearchAndDisplayAccessory.xib; sourceTree = "<group>"; };
|
E25AAC8A283D868D006E9E7F /* Classifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Classifier.swift; sourceTree = "<group>"; };
|
||||||
591FDD1F234E162000AA379E /* SearchAndDisplayAccessory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchAndDisplayAccessory.swift; sourceTree = "<group>"; };
|
E25AAC8C283D86CF006E9E7F /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||||
88A89ECD25AF420F00323B64 /* DispatchGroup+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchGroup+Extensions.swift"; sourceTree = "<group>"; };
|
E25AAC8F283D871E006E9E7F /* CGImagePropertyOrientation+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGImagePropertyOrientation+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
CE0A501024752A9800A9E753 /* TileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileImage.swift; sourceTree = "<group>"; };
|
E25AAC91283D8808006E9E7F /* CapData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapData.swift; sourceTree = "<group>"; };
|
||||||
CE0A5012247D745200A9E753 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
|
E25AAC93283D88A4006E9E7F /* Cap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cap.swift; sourceTree = "<group>"; };
|
||||||
CE56CECA209D81DD00932C01 /* Caps.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Caps.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
E25AAC95283E14DF006E9E7F /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = "<group>"; };
|
||||||
CE56CECD209D81DE00932C01 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
E25AAC9A283E3395006E9E7F /* CapRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapRowView.swift; sourceTree = "<group>"; };
|
||||||
CE56CED2209D81DE00932C01 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
E2EA00C4283EA72000F7B269 /* SortCriteria.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortCriteria.swift; sourceTree = "<group>"; };
|
||||||
CE56CED4209D81E000932C01 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortSelectionView.swift; sourceTree = "<group>"; };
|
||||||
CE56CED7209D81E000932C01 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortCaseRowView.swift; sourceTree = "<group>"; };
|
||||||
CE56CED9209D81E000932C01 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
E2EA00CD283EBEB600F7B269 /* SearchField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchField.swift; sourceTree = "<group>"; };
|
||||||
CE56CEE0209D83B200932C01 /* CapCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CapCell.swift; sourceTree = "<group>"; };
|
E2EA00D0283EDD6300F7B269 /* CameraManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraManager.swift; sourceTree = "<group>"; };
|
||||||
CE56CEE6209D83B300932C01 /* RoundedButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedButton.swift; sourceTree = "<group>"; };
|
E2EA00D2283EDDF700F7B269 /* CameraError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraError.swift; sourceTree = "<group>"; };
|
||||||
CE56CEE7209D83B300932C01 /* CameraController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraController.swift; sourceTree = "<group>"; };
|
E2EA00D4283EDFA200F7B269 /* FrameManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameManager.swift; sourceTree = "<group>"; };
|
||||||
CE56CEE8209D83B300932C01 /* UIAlertControllerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAlertControllerExtensions.swift; sourceTree = "<group>"; };
|
E2EA00D8283F5BB900F7B269 /* CameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
|
||||||
CE56CEEA209D83B400932C01 /* RoundedImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedImageView.swift; sourceTree = "<group>"; };
|
E2EA00DA283F5C0600F7B269 /* ContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewModel.swift; sourceTree = "<group>"; };
|
||||||
CE56CEEB209D83B400932C01 /* TableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TableView.swift; path = ../TableView.swift; sourceTree = "<group>"; };
|
E2EA00DC283F5C6A00F7B269 /* FrameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameView.swift; sourceTree = "<group>"; };
|
||||||
CE56CEEC209D83B400932C01 /* UIViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = "<group>"; };
|
E2EA00DE283F5CA000F7B269 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
|
||||||
CE56CEED209D83B400932C01 /* ViewControllerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerExtensions.swift; sourceTree = "<group>"; };
|
E2EA00E0283F658E00F7B269 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||||
CE56CEEE209D83B500932C01 /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
|
E2EA00E2283F662800F7B269 /* GridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridView.swift; sourceTree = "<group>"; };
|
||||||
CE56CEEF209D83B500932C01 /* ImageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCell.swift; sourceTree = "<group>"; };
|
E2EA00E4283F69DF00F7B269 /* SettingsStatisticRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStatisticRow.swift; sourceTree = "<group>"; };
|
||||||
CE56CEF0209D83B500932C01 /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
|
E2EA00E6283F6D0800F7B269 /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
CE56CEF1209D83B500932C01 /* Classifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Classifier.swift; sourceTree = "<group>"; };
|
E2EA00EA284109CC00F7B269 /* CGImage+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGImage+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
CE56CEF2209D83B600932C01 /* CropView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropView.swift; sourceTree = "<group>"; };
|
E2EA00EC2841170100F7B269 /* UIImage+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
CE56CEF3209D83B600932C01 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
E2EA00EE28420AA000F7B269 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
CE56CEF5209D83B600932C01 /* ImageSelector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageSelector.swift; sourceTree = "<group>"; };
|
E2EA00F228438E6B00F7B269 /* CapNameEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapNameEntryView.swift; sourceTree = "<group>"; };
|
||||||
CE56CEF6209D83B700932C01 /* PhotoCaptureHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureHandler.swift; sourceTree = "<group>"; };
|
|
||||||
CE56CEF7209D83B700932C01 /* UIImage+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Extensions.swift"; 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>"; };
|
|
||||||
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 */
|
||||||
CE56CEC7209D81DD00932C01 /* Frameworks */ = {
|
E25AAC75283D855D006E9E7F /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
CEB269572445DB56004B74B3 /* SQLite in Frameworks */,
|
E2EA00C3283E672A00F7B269 /* SFSafeSymbols in Frameworks */,
|
||||||
CE5B7D032458C921002E5C06 /* Reachability in Frameworks */,
|
E2EA00CA283EACB200F7B269 /* BottomSheet in Frameworks */,
|
||||||
|
E27E15E1283E418600F6804A /* CachedAsyncImage in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
CE56CEC1209D81DD00932C01 = {
|
E25AAC6F283D855D006E9E7F = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
CE56CECC209D81DD00932C01 /* Caps */,
|
E25AAC7A283D855D006E9E7F /* Caps */,
|
||||||
CE56CECB209D81DD00932C01 /* Products */,
|
E25AAC79283D855D006E9E7F /* Products */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
CE56CECB209D81DD00932C01 /* Products */ = {
|
E25AAC79283D855D006E9E7F /* Products */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
CE56CECA209D81DD00932C01 /* Caps.app */,
|
E25AAC78283D855D006E9E7F /* Caps.app */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
CE56CECC209D81DD00932C01 /* Caps */ = {
|
E25AAC7A283D855D006E9E7F /* Caps */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
CE56CECD209D81DE00932C01 /* AppDelegate.swift */,
|
E25AAC7B283D855D006E9E7F /* CapsApp.swift */,
|
||||||
CE56CED1209D81DE00932C01 /* Main.storyboard */,
|
E25AAC7D283D855D006E9E7F /* ContentView.swift */,
|
||||||
CEF3874D209D9378001C8D3C /* Capture */,
|
E2EA00CF283EDD2C00F7B269 /* Camera */,
|
||||||
CEF38750209D93D1001C8D3C /* Data */,
|
E25AAC97283E337C006E9E7F /* Views */,
|
||||||
CEF3874B209D932E001C8D3C /* View Components */,
|
E25AAC89283D8666006E9E7F /* Data */,
|
||||||
CEF3874F209D93A6001C8D3C /* Presentation */,
|
E25AAC7F283D855F006E9E7F /* Assets.xcassets */,
|
||||||
CEF3874C209D935E001C8D3C /* Extensions */,
|
E25AAC8E283D870F006E9E7F /* Extensions */,
|
||||||
CE56CEF3209D83B600932C01 /* Logger.swift */,
|
E25AAC8C283D86CF006E9E7F /* Logger.swift */,
|
||||||
CE56CEDF209D81FD00932C01 /* Support */,
|
E25AAC81283D855F006E9E7F /* Preview Content */,
|
||||||
);
|
);
|
||||||
path = Caps;
|
path = Caps;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
CE56CEDF209D81FD00932C01 /* Support */ = {
|
E25AAC81283D855F006E9E7F /* Preview Content */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
CE56CED4209D81E000932C01 /* Assets.xcassets */,
|
E25AAC82283D855F006E9E7F /* Preview Assets.xcassets */,
|
||||||
CE56CED6209D81E000932C01 /* LaunchScreen.storyboard */,
|
|
||||||
CE56CED9209D81E000932C01 /* Info.plist */,
|
|
||||||
);
|
);
|
||||||
name = Support;
|
path = "Preview Content";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
CEF3874B209D932E001C8D3C /* View Components */ = {
|
E25AAC89283D8666006E9E7F /* Data */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */,
|
E25AAC8A283D868D006E9E7F /* Classifier.swift */,
|
||||||
CE56CEF2209D83B600932C01 /* CropView.swift */,
|
E2EA00C4283EA72000F7B269 /* SortCriteria.swift */,
|
||||||
CE56CEEA209D83B400932C01 /* RoundedImageView.swift */,
|
E25AAC93283D88A4006E9E7F /* Cap.swift */,
|
||||||
CE56CEE6209D83B300932C01 /* RoundedButton.swift */,
|
E25AAC91283D8808006E9E7F /* CapData.swift */,
|
||||||
|
E25AAC95283E14DF006E9E7F /* Database.swift */,
|
||||||
);
|
);
|
||||||
path = "View Components";
|
path = Data;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
CEF3874C209D935E001C8D3C /* Extensions */ = {
|
E25AAC8E283D870F006E9E7F /* Extensions */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
CE56CEE8209D83B300932C01 /* UIAlertControllerExtensions.swift */,
|
E25AAC8F283D871E006E9E7F /* CGImagePropertyOrientation+Extensions.swift */,
|
||||||
CE6E4827246C304100570CB0 /* CGImagePropertyOrientation+Extensions.swift */,
|
E2EA00EC2841170100F7B269 /* UIImage+Extensions.swift */,
|
||||||
CE85AA17246B012B002D1074 /* Array+Extensions.swift */,
|
E2EA00E6283F6D0800F7B269 /* URL+Extensions.swift */,
|
||||||
CE85AA15246A96C3002D1074 /* UINavigationItem+Extensions.swift */,
|
E2EA00EA284109CC00F7B269 /* CGImage+Extensions.swift */,
|
||||||
CEB2695A2445E54E004B74B3 /* UIColor+Extensions.swift */,
|
E2EA00EE28420AA000F7B269 /* Data+Extensions.swift */,
|
||||||
CE56CEF7209D83B700932C01 /* UIImage+Extensions.swift */,
|
|
||||||
CE56CEEC209D83B400932C01 /* UIViewExtensions.swift */,
|
|
||||||
CE56CEED209D83B400932C01 /* ViewControllerExtensions.swift */,
|
|
||||||
88A89ECD25AF420F00323B64 /* DispatchGroup+Extensions.swift */,
|
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
CEF3874D209D9378001C8D3C /* Capture */ = {
|
E25AAC97283E337C006E9E7F /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
CE56CEE7209D83B300932C01 /* CameraController.swift */,
|
E25AAC9A283E3395006E9E7F /* CapRowView.swift */,
|
||||||
CE56CEEE209D83B500932C01 /* CameraView.swift */,
|
E2EA00E2283F662800F7B269 /* GridView.swift */,
|
||||||
CE56CEF6209D83B700932C01 /* PhotoCaptureHandler.swift */,
|
E2EA00E0283F658E00F7B269 /* SettingsView.swift */,
|
||||||
|
E2EA00E4283F69DF00F7B269 /* SettingsStatisticRow.swift */,
|
||||||
|
E2EA00CD283EBEB600F7B269 /* SearchField.swift */,
|
||||||
|
E2EA00F228438E6B00F7B269 /* CapNameEntryView.swift */,
|
||||||
|
E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */,
|
||||||
|
E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */,
|
||||||
);
|
);
|
||||||
path = Capture;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
CEF3874F209D93A6001C8D3C /* Presentation */ = {
|
E2EA00CF283EDD2C00F7B269 /* Camera */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
CE56CEEB209D83B400932C01 /* TableView.swift */,
|
E2EA00D0283EDD6300F7B269 /* CameraManager.swift */,
|
||||||
591FDD1D234E151600AA379E /* SearchAndDisplayAccessory.xib */,
|
E2EA00D8283F5BB900F7B269 /* CameraView.swift */,
|
||||||
591FDD1F234E162000AA379E /* SearchAndDisplayAccessory.swift */,
|
E2EA00DA283F5C0600F7B269 /* ContentViewModel.swift */,
|
||||||
59158B1721E4C9AC00D90CB0 /* NavigationController.swift */,
|
E2EA00DC283F5C6A00F7B269 /* FrameView.swift */,
|
||||||
CE56CEE0209D83B200932C01 /* CapCell.swift */,
|
E2EA00D4283EDFA200F7B269 /* FrameManager.swift */,
|
||||||
CE56CEF5209D83B600932C01 /* ImageSelector.swift */,
|
E2EA00D2283EDDF700F7B269 /* CameraError.swift */,
|
||||||
CE56CEEF209D83B500932C01 /* ImageCell.swift */,
|
E2EA00DE283F5CA000F7B269 /* ErrorView.swift */,
|
||||||
5904C3392199C9FA0046A573 /* SortController.swift */,
|
|
||||||
59158B1521E37B0200D90CB0 /* GridViewController.swift */,
|
|
||||||
);
|
);
|
||||||
path = Presentation;
|
path = Camera;
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
CEF38750209D93D1001C8D3C /* Data */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
CE56CEF1209D83B500932C01 /* Classifier.swift */,
|
|
||||||
591832CD21A2A97E00E5987D /* Cap.swift */,
|
|
||||||
CE56CEF0209D83B500932C01 /* Storage.swift */,
|
|
||||||
CE5B7CFB24562673002E5C06 /* Download.swift */,
|
|
||||||
CE5B7CFD245626D3002E5C06 /* Upload.swift */,
|
|
||||||
CEB269582445DB72004B74B3 /* Database.swift */,
|
|
||||||
CE0A5012247D745200A9E753 /* Colors.swift */,
|
|
||||||
CE0A501024752A9800A9E753 /* TileImage.swift */,
|
|
||||||
);
|
|
||||||
path = Data;
|
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
CE56CEC9209D81DD00932C01 /* Caps */ = {
|
E25AAC77283D855D006E9E7F /* Caps */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = CE56CEDC209D81E000932C01 /* Build configuration list for PBXNativeTarget "Caps" */;
|
buildConfigurationList = E25AAC86283D855F006E9E7F /* Build configuration list for PBXNativeTarget "Caps" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
CE56CEC6209D81DD00932C01 /* Sources */,
|
E25AAC74283D855D006E9E7F /* Sources */,
|
||||||
CE56CEC7209D81DD00932C01 /* Frameworks */,
|
E25AAC75283D855D006E9E7F /* Frameworks */,
|
||||||
CE56CEC8209D81DD00932C01 /* Resources */,
|
E25AAC76283D855D006E9E7F /* Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@ -231,141 +200,110 @@
|
|||||||
);
|
);
|
||||||
name = Caps;
|
name = Caps;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
CEB269562445DB56004B74B3 /* SQLite */,
|
E27E15E0283E418600F6804A /* CachedAsyncImage */,
|
||||||
CE5B7D022458C921002E5C06 /* Reachability */,
|
E2EA00C2283E672A00F7B269 /* SFSafeSymbols */,
|
||||||
|
E2EA00C9283EACB200F7B269 /* BottomSheet */,
|
||||||
);
|
);
|
||||||
productName = CapCollector;
|
productName = Caps;
|
||||||
productReference = CE56CECA209D81DD00932C01 /* Caps.app */;
|
productReference = E25AAC78283D855D006E9E7F /* Caps.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
CE56CEC2209D81DD00932C01 /* Project object */ = {
|
E25AAC70283D855D006E9E7F /* Project object */ = {
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
LastSwiftUpdateCheck = 0940;
|
BuildIndependentTargetsInParallel = 1;
|
||||||
LastUpgradeCheck = 1200;
|
LastSwiftUpdateCheck = 1340;
|
||||||
ORGANIZATIONNAME = CH;
|
LastUpgradeCheck = 1340;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
CE56CEC9209D81DD00932C01 = {
|
E25AAC77283D855D006E9E7F = {
|
||||||
CreatedOnToolsVersion = 9.4;
|
CreatedOnToolsVersion = 13.4;
|
||||||
LastSwiftMigration = 1100;
|
|
||||||
SystemCapabilities = {
|
|
||||||
com.apple.BackgroundModes = {
|
|
||||||
enabled = 0;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
buildConfigurationList = E25AAC73283D855D006E9E7F /* Build configuration list for PBXProject "Caps" */;
|
||||||
};
|
compatibilityVersion = "Xcode 13.0";
|
||||||
buildConfigurationList = CE56CEC5209D81DD00932C01 /* Build configuration list for PBXProject "Caps" */;
|
|
||||||
compatibilityVersion = "Xcode 9.3";
|
|
||||||
developmentRegion = en;
|
developmentRegion = en;
|
||||||
hasScannedForEncodings = 0;
|
hasScannedForEncodings = 0;
|
||||||
knownRegions = (
|
knownRegions = (
|
||||||
en,
|
en,
|
||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = CE56CEC1209D81DD00932C01;
|
mainGroup = E25AAC6F283D855D006E9E7F;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
CEB269552445DB56004B74B3 /* XCRemoteSwiftPackageReference "SQLite" */,
|
E27E15DF283E418600F6804A /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */,
|
||||||
CE5B7D012458C921002E5C06 /* XCRemoteSwiftPackageReference "Reachability" */,
|
E2EA00C1283E672A00F7B269 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
|
||||||
|
E2EA00C8283EACB200F7B269 /* XCRemoteSwiftPackageReference "bottom-sheet" */,
|
||||||
);
|
);
|
||||||
productRefGroup = CE56CECB209D81DD00932C01 /* Products */;
|
productRefGroup = E25AAC79283D855D006E9E7F /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
CE56CEC9209D81DD00932C01 /* Caps */,
|
E25AAC77283D855D006E9E7F /* Caps */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
CE56CEC8209D81DD00932C01 /* Resources */ = {
|
E25AAC76283D855D006E9E7F /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
CE56CED8209D81E000932C01 /* LaunchScreen.storyboard in Resources */,
|
E25AAC83283D855F006E9E7F /* Preview Assets.xcassets in Resources */,
|
||||||
591FDD1E234E151600AA379E /* SearchAndDisplayAccessory.xib in Resources */,
|
E25AAC80283D855F006E9E7F /* Assets.xcassets in Resources */,
|
||||||
CE56CED5209D81E000932C01 /* Assets.xcassets in Resources */,
|
|
||||||
CE56CED3209D81DE00932C01 /* Main.storyboard in Resources */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
CE56CEC6209D81DD00932C01 /* Sources */ = {
|
E25AAC74283D855D006E9E7F /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
CE56CF09209D83B800932C01 /* Classifier.swift in Sources */,
|
E25AAC7E283D855D006E9E7F /* ContentView.swift in Sources */,
|
||||||
CE0A5013247D745200A9E753 /* Colors.swift in Sources */,
|
E2EA00F328438E6B00F7B269 /* CapNameEntryView.swift in Sources */,
|
||||||
5904C33A2199C9FA0046A573 /* SortController.swift in Sources */,
|
E25AAC8B283D868D006E9E7F /* Classifier.swift in Sources */,
|
||||||
CE56CF0B209D83B800932C01 /* Logger.swift in Sources */,
|
E25AAC94283D88A4006E9E7F /* Cap.swift in Sources */,
|
||||||
CE56CF04209D83B800932C01 /* UIViewExtensions.swift in Sources */,
|
E2EA00D9283F5BB900F7B269 /* CameraView.swift in Sources */,
|
||||||
59158B1821E4C9AC00D90CB0 /* NavigationController.swift in Sources */,
|
E2EA00E3283F662800F7B269 /* GridView.swift in Sources */,
|
||||||
591832CE21A2A97E00E5987D /* Cap.swift in Sources */,
|
E2EA00EB284109CC00F7B269 /* CGImage+Extensions.swift in Sources */,
|
||||||
CE56CF08209D83B800932C01 /* Storage.swift in Sources */,
|
E2EA00DF283F5CA000F7B269 /* ErrorView.swift in Sources */,
|
||||||
CE56CF0F209D83B800932C01 /* UIImage+Extensions.swift in Sources */,
|
E2EA00D5283EDFA200F7B269 /* FrameManager.swift in Sources */,
|
||||||
CE5B7CFC24562673002E5C06 /* Download.swift in Sources */,
|
E25AAC90283D871E006E9E7F /* CGImagePropertyOrientation+Extensions.swift in Sources */,
|
||||||
CE85AA18246B012B002D1074 /* Array+Extensions.swift in Sources */,
|
E2EA00CE283EBEB600F7B269 /* SearchField.swift in Sources */,
|
||||||
CE56CF03209D83B800932C01 /* TableView.swift in Sources */,
|
E2EA00C7283EAA0100F7B269 /* SortSelectionView.swift in Sources */,
|
||||||
CEB2695B2445E54E004B74B3 /* UIColor+Extensions.swift in Sources */,
|
E2EA00DD283F5C6A00F7B269 /* FrameView.swift in Sources */,
|
||||||
591FDD20234E162000AA379E /* SearchAndDisplayAccessory.swift in Sources */,
|
E2EA00EF28420AA000F7B269 /* Data+Extensions.swift in Sources */,
|
||||||
59158B1621E37B0200D90CB0 /* GridViewController.swift in Sources */,
|
E2EA00C5283EA72000F7B269 /* SortCriteria.swift in Sources */,
|
||||||
CE5B7CFE245626D3002E5C06 /* Upload.swift in Sources */,
|
E25AAC7C283D855D006E9E7F /* CapsApp.swift in Sources */,
|
||||||
CE56CECE209D81DE00932C01 /* AppDelegate.swift in Sources */,
|
E2EA00D1283EDD6300F7B269 /* CameraManager.swift in Sources */,
|
||||||
CE56CF0D209D83B800932C01 /* ImageSelector.swift in Sources */,
|
E25AAC9B283E3395006E9E7F /* CapRowView.swift in Sources */,
|
||||||
CE56CEFF209D83B800932C01 /* CameraController.swift in Sources */,
|
E2EA00DB283F5C0600F7B269 /* ContentViewModel.swift in Sources */,
|
||||||
CE56CF05209D83B800932C01 /* ViewControllerExtensions.swift in Sources */,
|
E2EA00CC283EB43E00F7B269 /* SortCaseRowView.swift in Sources */,
|
||||||
CE56CF0E209D83B800932C01 /* PhotoCaptureHandler.swift in Sources */,
|
E2EA00E7283F6D0800F7B269 /* URL+Extensions.swift in Sources */,
|
||||||
CE56CEFE209D83B800932C01 /* RoundedButton.swift in Sources */,
|
E2EA00D3283EDDF700F7B269 /* CameraError.swift in Sources */,
|
||||||
CE56CF07209D83B800932C01 /* ImageCell.swift in Sources */,
|
E25AAC92283D8808006E9E7F /* CapData.swift in Sources */,
|
||||||
CE56CF06209D83B800932C01 /* CameraView.swift in Sources */,
|
E25AAC96283E14DF006E9E7F /* Database.swift in Sources */,
|
||||||
CE56CF0A209D83B800932C01 /* CropView.swift in Sources */,
|
E25AAC8D283D86CF006E9E7F /* Logger.swift in Sources */,
|
||||||
5904C33C2199D0260046A573 /* AlwaysShowPopup.swift in Sources */,
|
E2EA00ED2841170100F7B269 /* UIImage+Extensions.swift in Sources */,
|
||||||
CE85AA16246A96C3002D1074 /* UINavigationItem+Extensions.swift in Sources */,
|
E2EA00E5283F69DF00F7B269 /* SettingsStatisticRow.swift in Sources */,
|
||||||
CEB269592445DB72004B74B3 /* Database.swift in Sources */,
|
E2EA00E1283F658E00F7B269 /* SettingsView.swift in Sources */,
|
||||||
CE56CF02209D83B800932C01 /* RoundedImageView.swift in Sources */,
|
|
||||||
88A89ECE25AF420F00323B64 /* DispatchGroup+Extensions.swift in Sources */,
|
|
||||||
CE0A501124752A9800A9E753 /* TileImage.swift in Sources */,
|
|
||||||
CE56CEF8209D83B800932C01 /* CapCell.swift in Sources */,
|
|
||||||
CE6E4828246C304100570CB0 /* CGImagePropertyOrientation+Extensions.swift in Sources */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXVariantGroup section */
|
|
||||||
CE56CED1209D81DE00932C01 /* Main.storyboard */ = {
|
|
||||||
isa = PBXVariantGroup;
|
|
||||||
children = (
|
|
||||||
CE56CED2209D81DE00932C01 /* Base */,
|
|
||||||
);
|
|
||||||
name = Main.storyboard;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
CE56CED6209D81E000932C01 /* LaunchScreen.storyboard */ = {
|
|
||||||
isa = PBXVariantGroup;
|
|
||||||
children = (
|
|
||||||
CE56CED7209D81E000932C01 /* Base */,
|
|
||||||
);
|
|
||||||
name = LaunchScreen.storyboard;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXVariantGroup section */
|
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
CE56CEDA209D81E000932C01 /* Debug */ = {
|
E25AAC84283D855F006E9E7F /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
@ -391,7 +329,6 @@
|
|||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
@ -410,8 +347,9 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
@ -419,14 +357,13 @@
|
|||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
CE56CEDB209D81E000932C01 /* Release */ = {
|
E25AAC85283D855F006E9E7F /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||||
CLANG_CXX_LIBRARY = "libc++";
|
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CLANG_ENABLE_OBJC_ARC = YES;
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||||
@ -452,7 +389,6 @@
|
|||||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
@ -465,8 +401,9 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
@ -474,47 +411,63 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
CE56CEDD209D81E000932C01 /* Debug */ = {
|
E25AAC87283D855F006E9E7F /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_ASSET_PATHS = "\"Caps/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||||
INFOPLIST_FILE = "$(SRCROOT)/Caps/Info.plist";
|
ENABLE_PREVIEWS = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_NSCameraUsageDescription = "Take images to identify matching caps and register new ones";
|
||||||
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.4;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.Caps;
|
PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.Caps;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE = "";
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
CE56CEDE209D81E000932C01 /* Release */ = {
|
E25AAC88283D855F006E9E7F /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_ASSET_PATHS = "\"Caps/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||||
INFOPLIST_FILE = "$(SRCROOT)/Caps/Info.plist";
|
ENABLE_PREVIEWS = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_KEY_NSCameraUsageDescription = "Take images to identify matching caps and register new ones";
|
||||||
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.4;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.Caps;
|
PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.Caps;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE = "";
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
};
|
};
|
||||||
@ -523,20 +476,20 @@
|
|||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
CE56CEC5209D81DD00932C01 /* Build configuration list for PBXProject "Caps" */ = {
|
E25AAC73283D855D006E9E7F /* Build configuration list for PBXProject "Caps" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
CE56CEDA209D81E000932C01 /* Debug */,
|
E25AAC84283D855F006E9E7F /* Debug */,
|
||||||
CE56CEDB209D81E000932C01 /* Release */,
|
E25AAC85283D855F006E9E7F /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
CE56CEDC209D81E000932C01 /* Build configuration list for PBXNativeTarget "Caps" */ = {
|
E25AAC86283D855F006E9E7F /* Build configuration list for PBXNativeTarget "Caps" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
CE56CEDD209D81E000932C01 /* Debug */,
|
E25AAC87283D855F006E9E7F /* Debug */,
|
||||||
CE56CEDE209D81E000932C01 /* Release */,
|
E25AAC88283D855F006E9E7F /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
@ -544,36 +497,49 @@
|
|||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
CE5B7D012458C921002E5C06 /* XCRemoteSwiftPackageReference "Reachability" */ = {
|
E27E15DF283E418600F6804A /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/ashleymills/Reachability.swift";
|
repositoryURL = "https://github.com/lorenzofiamingo/swiftui-cached-async-image";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMajorVersion;
|
kind = upToNextMajorVersion;
|
||||||
minimumVersion = 5.0.0;
|
minimumVersion = 2.0.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
CEB269552445DB56004B74B3 /* XCRemoteSwiftPackageReference "SQLite" */ = {
|
E2EA00C1283E672A00F7B269 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/stephencelis/SQLite.swift";
|
repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMajorVersion;
|
kind = upToNextMajorVersion;
|
||||||
minimumVersion = 0.12.2;
|
minimumVersion = 3.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
E2EA00C8283EACB200F7B269 /* XCRemoteSwiftPackageReference "bottom-sheet" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/weitieda/bottom-sheet";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 1.0.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
CE5B7D022458C921002E5C06 /* Reachability */ = {
|
E27E15E0283E418600F6804A /* CachedAsyncImage */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = CE5B7D012458C921002E5C06 /* XCRemoteSwiftPackageReference "Reachability" */;
|
package = E27E15DF283E418600F6804A /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */;
|
||||||
productName = Reachability;
|
productName = CachedAsyncImage;
|
||||||
};
|
};
|
||||||
CEB269562445DB56004B74B3 /* SQLite */ = {
|
E2EA00C2283E672A00F7B269 /* SFSafeSymbols */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = CEB269552445DB56004B74B3 /* XCRemoteSwiftPackageReference "SQLite" */;
|
package = E2EA00C1283E672A00F7B269 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;
|
||||||
productName = SQLite;
|
productName = SFSafeSymbols;
|
||||||
|
};
|
||||||
|
E2EA00C9283EACB200F7B269 /* BottomSheet */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = E2EA00C8283EACB200F7B269 /* XCRemoteSwiftPackageReference "bottom-sheet" */;
|
||||||
|
productName = BottomSheet;
|
||||||
};
|
};
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = CE56CEC2209D81DD00932C01 /* Project object */;
|
rootObject = E25AAC70283D855D006E9E7F /* Project object */;
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,32 @@
|
|||||||
{
|
{
|
||||||
"object": {
|
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"package": "Reachability",
|
"identity" : "bottom-sheet",
|
||||||
"repositoryURL": "https://github.com/ashleymills/Reachability.swift",
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/weitieda/bottom-sheet",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch": null,
|
"revision" : "4e074d49f3148577ac66cf47b85a99d016480d01",
|
||||||
"revision": "c01bbdf2d633cf049ae1ed1a68a2020a8bda32e2",
|
"version" : "1.0.10"
|
||||||
"version": "5.1.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"package": "SQLite.swift",
|
"identity" : "sfsafesymbols",
|
||||||
"repositoryURL": "https://github.com/stephencelis/SQLite.swift",
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch": null,
|
"revision" : "c8c33d947d8a1c883aa19fd24e14fd738b06e369",
|
||||||
"revision": "5f5ad81ac0d0a0f3e56e39e646e8423c617df523",
|
"version" : "3.3.2"
|
||||||
"version": "0.13.2"
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"version": 1
|
{
|
||||||
|
"identity" : "swiftui-cached-async-image",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/lorenzofiamingo/swiftui-cached-async-image",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "eeb1565d780d1b75d045e21b5ca2a1e3650b0fc2",
|
||||||
|
"version" : "2.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
}
|
}
|
||||||
|
78
Caps.xcodeproj/xcshareddata/xcschemes/Caps.xcscheme
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1340"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "E25AAC77283D855D006E9E7F"
|
||||||
|
BuildableName = "Caps.app"
|
||||||
|
BlueprintName = "Caps"
|
||||||
|
ReferencedContainer = "container:Caps.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "E25AAC77283D855D006E9E7F"
|
||||||
|
BuildableName = "Caps.app"
|
||||||
|
BlueprintName = "Caps"
|
||||||
|
ReferencedContainer = "container:Caps.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "E25AAC77283D855D006E9E7F"
|
||||||
|
BuildableName = "Caps.app"
|
||||||
|
BlueprintName = "Caps"
|
||||||
|
ReferencedContainer = "container:Caps.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
@ -1,19 +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>SchemeUserState</key>
|
|
||||||
<dict>
|
|
||||||
<key>CapCollector.xcscheme</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
</dict>
|
|
||||||
<key>CapCollector.xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Bucket
|
<Bucket
|
||||||
uuid = "B1F05379-5C9B-42D8-94ED-BB89F9571BE1"
|
uuid = "B7EB9D95-1101-4B5E-A288-5C0C223F7E4C"
|
||||||
type = "1"
|
type = "1"
|
||||||
version = "2.0">
|
version = "2.0">
|
||||||
</Bucket>
|
</Bucket>
|
@ -4,52 +4,18 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>SchemeUserState</key>
|
<key>SchemeUserState</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CapCollector.xcscheme_^#shared#^_</key>
|
<key>Caps.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
</dict>
|
|
||||||
<key>SQLite (Playground) 1.xcscheme</key>
|
|
||||||
<dict>
|
|
||||||
<key>isShown</key>
|
|
||||||
<false/>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>3</integer>
|
|
||||||
</dict>
|
|
||||||
<key>SQLite (Playground) 2.xcscheme</key>
|
|
||||||
<dict>
|
|
||||||
<key>isShown</key>
|
|
||||||
<false/>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>4</integer>
|
|
||||||
</dict>
|
|
||||||
<key>SQLite (Playground) 3.xcscheme</key>
|
|
||||||
<dict>
|
|
||||||
<key>isShown</key>
|
|
||||||
<false/>
|
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SQLite (Playground) 4.xcscheme</key>
|
|
||||||
<dict>
|
|
||||||
<key>isShown</key>
|
|
||||||
<false/>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>5</integer>
|
|
||||||
</dict>
|
</dict>
|
||||||
<key>SQLite (Playground) 5.xcscheme</key>
|
<key>SuppressBuildableAutocreation</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>isShown</key>
|
<key>E25AAC77283D855D006E9E7F</key>
|
||||||
<false/>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>6</integer>
|
|
||||||
</dict>
|
|
||||||
<key>SQLite (Playground).xcscheme</key>
|
|
||||||
<dict>
|
<dict>
|
||||||
<key>isShown</key>
|
<key>primary</key>
|
||||||
<false/>
|
<true/>
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>2</integer>
|
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
@ -1,19 +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>SchemeUserState</key>
|
|
||||||
<dict>
|
|
||||||
<key>CapCollector.xcscheme</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>3</integer>
|
|
||||||
</dict>
|
|
||||||
<key>CapCollector.xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>3</integer>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
@ -1,35 +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>SchemeUserState</key>
|
|
||||||
<dict>
|
|
||||||
<key>CapCollector.xcscheme_^#shared#^_</key>
|
|
||||||
<dict>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>0</integer>
|
|
||||||
</dict>
|
|
||||||
<key>SQLite (Playground) 1.xcscheme</key>
|
|
||||||
<dict>
|
|
||||||
<key>isShown</key>
|
|
||||||
<false/>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>2</integer>
|
|
||||||
</dict>
|
|
||||||
<key>SQLite (Playground) 2.xcscheme</key>
|
|
||||||
<dict>
|
|
||||||
<key>isShown</key>
|
|
||||||
<false/>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>3</integer>
|
|
||||||
</dict>
|
|
||||||
<key>SQLite (Playground).xcscheme</key>
|
|
||||||
<dict>
|
|
||||||
<key>isShown</key>
|
|
||||||
<false/>
|
|
||||||
<key>orderHint</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
@ -1,126 +0,0 @@
|
|||||||
//
|
|
||||||
// AppDelegate.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 31.01.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import CoreData
|
|
||||||
|
|
||||||
import Reachability
|
|
||||||
|
|
||||||
#warning("ImageSelector: Allow deletion and moving of an image of a cap")
|
|
||||||
#warning("ImageSelector: Show icons for failed downloads")
|
|
||||||
#warning("GridController: Allow sorting of caps by color")
|
|
||||||
#warning("GridController: Reorder caps by dragging")
|
|
||||||
#warning("TableView: Fix blur background of search bar after transition")
|
|
||||||
#warning("TableView: Add banner to jump down to unmatched caps / bottom")
|
|
||||||
#warning("Only show total size and percentage for classifier download")
|
|
||||||
#warning("After classifier download, test is not started")
|
|
||||||
#warning("Add result info when update button is pressed")
|
|
||||||
var shouldLaunchCamera = false
|
|
||||||
|
|
||||||
var app: AppDelegate!
|
|
||||||
|
|
||||||
private let unlockCode = 3849
|
|
||||||
|
|
||||||
@UIApplicationMain
|
|
||||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
|
||||||
|
|
||||||
// MARK: Static Properties
|
|
||||||
|
|
||||||
/// Main tint color of the app
|
|
||||||
static let tintColor = UIColor(red: 122/255, green: 155/255, blue: 41/255, alpha: 1)
|
|
||||||
|
|
||||||
var window: UIWindow?
|
|
||||||
|
|
||||||
var mainStoryboard: UIStoryboard { .init(name: "Main", bundle: nil) }
|
|
||||||
|
|
||||||
let documentsFolder = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
|
||||||
|
|
||||||
var database: Database!
|
|
||||||
|
|
||||||
var reachability: Reachability!
|
|
||||||
|
|
||||||
var dbUrl: URL {
|
|
||||||
documentsFolder.appendingPathComponent("db.sqlite3")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Indicate if the user has write permissions.
|
|
||||||
private(set) var isUnlocked: Bool {
|
|
||||||
get {
|
|
||||||
UserDefaults.standard.bool(forKey: "unlocked")
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
UserDefaults.standard.set(newValue, forKey: "unlocked")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let serverUrl = URL(string: "https://christophhagen.de:6000")!
|
|
||||||
|
|
||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
|
||||||
app = self
|
|
||||||
|
|
||||||
reachability = try! Reachability()
|
|
||||||
|
|
||||||
//resetToFactoryState()
|
|
||||||
|
|
||||||
database = Database(url: dbUrl, server: serverUrl, storageFolder: documentsFolder)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func resetToFactoryState() {
|
|
||||||
for path in try! FileManager.default.contentsOfDirectory(at: documentsFolder, includingPropertiesForKeys: nil) {
|
|
||||||
try! FileManager.default.removeItem(at: path)
|
|
||||||
}
|
|
||||||
UserDefaults.standard.removeObject(forKey: Classifier.userDefaultsKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func lock() {
|
|
||||||
isUnlocked = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkUnlock(with pin: Int) -> Bool {
|
|
||||||
isUnlocked = pin == unlockCode
|
|
||||||
return isUnlocked
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleShortCutItem(_ shortcutItem: UIApplicationShortcutItem) -> Bool {
|
|
||||||
log("Shortcut pressed")
|
|
||||||
shouldLaunchCamera = true
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func applicationDidBecomeActive(_ application: UIApplication) {
|
|
||||||
guard shouldLaunchCamera else { return }
|
|
||||||
shouldLaunchCamera = false
|
|
||||||
if let c = (frontmostViewController as? UINavigationController)?.topViewController as? TableView {
|
|
||||||
c.showCameraView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Called when the user activates your application by selecting a shortcut on the home screen, except when
|
|
||||||
application(_:,willFinishLaunchingWithOptions:) or application(_:didFinishLaunchingWithOptions) returns `false`.
|
|
||||||
You should handle the shortcut in those callbacks and return `false` if possible. In that case, this
|
|
||||||
callback is used if your application is already launched in the background.
|
|
||||||
*/
|
|
||||||
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
|
||||||
let handledShortCutItem = handleShortCutItem(shortcutItem)
|
|
||||||
completionHandler(handledShortCutItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
var frontmostViewController: UIViewController? {
|
|
||||||
var controller = window?.rootViewController
|
|
||||||
while let presentedViewController = controller?.presentedViewController {
|
|
||||||
controller = presentedViewController
|
|
||||||
}
|
|
||||||
return controller
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AppDelegate: Logger { }
|
|
11
Caps/Assets.xcassets/AccentColor.colorset/Contents.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -1,107 +1,107 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "bottle-cap40.png",
|
"filename" : "bottle-cap40.png",
|
||||||
"scale" : "2x"
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "bottle-cap60.png",
|
"filename" : "bottle-cap60.png",
|
||||||
"scale" : "3x"
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "bottle-cap58.png",
|
"filename" : "bottle-cap58.png",
|
||||||
"scale" : "2x"
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "29x29",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "bottle-cap87.png",
|
"filename" : "bottle-cap87.png",
|
||||||
"scale" : "3x"
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "bottle-cap80.png",
|
"filename" : "bottle-cap80.png",
|
||||||
"scale" : "2x"
|
"idiom" : "iphone",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "40x40",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "bottle-cap120.png",
|
"filename" : "bottle-cap120.png",
|
||||||
"scale" : "3x"
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "60x60",
|
|
||||||
"idiom" : "iphone",
|
|
||||||
"filename" : "bottle-cap120-1.png",
|
"filename" : "bottle-cap120-1.png",
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"size" : "60x60",
|
|
||||||
"idiom" : "iphone",
|
"idiom" : "iphone",
|
||||||
"filename" : "bottle-cap180.png",
|
"scale" : "2x",
|
||||||
"scale" : "3x"
|
"size" : "60x60"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "bottle-cap180-1.png",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"scale" : "3x",
|
||||||
|
"size" : "60x60"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"size" : "20x20",
|
"scale" : "1x",
|
||||||
"scale" : "1x"
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"size" : "20x20",
|
"scale" : "2x",
|
||||||
"scale" : "2x"
|
"size" : "20x20"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"size" : "29x29",
|
"scale" : "1x",
|
||||||
"scale" : "1x"
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"size" : "29x29",
|
"scale" : "2x",
|
||||||
"scale" : "2x"
|
"size" : "29x29"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"size" : "40x40",
|
"scale" : "1x",
|
||||||
"scale" : "1x"
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"size" : "40x40",
|
"scale" : "2x",
|
||||||
"scale" : "2x"
|
"size" : "40x40"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"size" : "76x76",
|
"scale" : "1x",
|
||||||
"scale" : "1x"
|
"size" : "76x76"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"size" : "76x76",
|
"scale" : "2x",
|
||||||
"scale" : "2x"
|
"size" : "76x76"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "ipad",
|
"idiom" : "ipad",
|
||||||
"size" : "83.5x83.5",
|
"scale" : "2x",
|
||||||
"scale" : "2x"
|
"size" : "83.5x83.5"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "1024x1024",
|
|
||||||
"idiom" : "ios-marketing",
|
|
||||||
"filename" : "bottle-cap1024.png",
|
"filename" : "bottle-cap1024.png",
|
||||||
"scale" : "1x"
|
"idiom" : "ios-marketing",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "1024x1024"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
"version" : 1,
|
"author" : "xcode",
|
||||||
"author" : "xcode"
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"info" : {
|
"info" : {
|
||||||
"version" : 1,
|
"author" : "xcode",
|
||||||
"author" : "xcode"
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "camera.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
BIN
Caps/Assets.xcassets/camera.imageset/camera.png
vendored
Before Width: | Height: | Size: 27 KiB |
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "camera_square.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 27 KiB |
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "cancel.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
BIN
Caps/Assets.xcassets/cancel.imageset/cancel.png
vendored
Before Width: | Height: | Size: 17 KiB |
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "launch.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
BIN
Caps/Assets.xcassets/launch.imageset/launch.png
vendored
Before Width: | Height: | Size: 298 KiB |
@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"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
Caps/Assets.xcassets/mosaic.imageset/picture28.png
vendored
Before Width: | Height: | Size: 2.3 KiB |
BIN
Caps/Assets.xcassets/mosaic.imageset/picture56.png
vendored
Before Width: | Height: | Size: 2.8 KiB |
BIN
Caps/Assets.xcassets/mosaic.imageset/picture84.png
vendored
Before Width: | Height: | Size: 3.3 KiB |
@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"images" : [
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"filename" : "search_icon.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "2x"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idiom" : "universal",
|
|
||||||
"scale" : "3x"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"version" : 1,
|
|
||||||
"author" : "xcode"
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 20 KiB |
@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 3.0 KiB |
@ -1,46 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14105" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
|
||||||
<device id="retina4_7" orientation="portrait">
|
|
||||||
<adaptation id="fullscreen"/>
|
|
||||||
</device>
|
|
||||||
<dependencies>
|
|
||||||
<deployment identifier="iOS"/>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
|
|
||||||
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
|
||||||
</dependencies>
|
|
||||||
<scenes>
|
|
||||||
<!--View Controller-->
|
|
||||||
<scene sceneID="EHf-IW-A2E">
|
|
||||||
<objects>
|
|
||||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
|
||||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<subviews>
|
|
||||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="launch" translatesAutoresizingMaskIntoConstraints="NO" id="SQk-Vr-WK2">
|
|
||||||
<rect key="frame" x="62" y="218.5" width="250" height="250"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" constant="250" id="PfE-No-7DY"/>
|
|
||||||
<constraint firstAttribute="width" secondItem="SQk-Vr-WK2" secondAttribute="height" multiplier="1:1" id="b99-Ji-vq3"/>
|
|
||||||
</constraints>
|
|
||||||
</imageView>
|
|
||||||
</subviews>
|
|
||||||
<color key="backgroundColor" red="0.41960784313725491" green="0.57647058823529407" blue="0.37254901960784315" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="SQk-Vr-WK2" firstAttribute="centerX" secondItem="6Tk-OE-BBY" secondAttribute="centerX" id="8lU-QA-M23"/>
|
|
||||||
<constraint firstItem="SQk-Vr-WK2" firstAttribute="centerY" secondItem="6Tk-OE-BBY" secondAttribute="centerY" id="M5A-L2-2tA"/>
|
|
||||||
</constraints>
|
|
||||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
|
||||||
</view>
|
|
||||||
</viewController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="53" y="375"/>
|
|
||||||
</scene>
|
|
||||||
</scenes>
|
|
||||||
<resources>
|
|
||||||
<image name="launch" width="1024" height="1024"/>
|
|
||||||
</resources>
|
|
||||||
</document>
|
|
@ -1,429 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="qlf-I7-aOI">
|
|
||||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
|
||||||
<dependencies>
|
|
||||||
<deployment identifier="iOS"/>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
|
||||||
</dependencies>
|
|
||||||
<scenes>
|
|
||||||
<!--Main Image-->
|
|
||||||
<scene sceneID="FO1-cz-6Q6">
|
|
||||||
<objects>
|
|
||||||
<viewController storyboardIdentifier="ImageSelector" id="xXN-Zh-iCd" customClass="ImageSelector" customModule="CapCollector" customModuleProvider="target" sceneMemberID="viewController">
|
|
||||||
<view key="view" contentMode="scaleToFill" id="7RW-nN-0XQ">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<subviews>
|
|
||||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Vx1-tM-8mD" userLabel="Status Bar View">
|
|
||||||
<rect key="frame" x="0.0" y="24" width="375" height="20"/>
|
|
||||||
<color key="backgroundColor" red="0.14168914909999999" green="0.14168914909999999" blue="0.14168914909999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
</view>
|
|
||||||
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="KLt-lS-0bs">
|
|
||||||
<rect key="frame" x="0.0" y="44" width="375" height="623"/>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="10" minimumInteritemSpacing="0.0" id="m0z-hV-bp1">
|
|
||||||
<size key="itemSize" width="125" height="125"/>
|
|
||||||
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
|
||||||
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
|
||||||
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
|
|
||||||
</collectionViewFlowLayout>
|
|
||||||
<cells>
|
|
||||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Image" id="a3h-Rh-cyS" customClass="ImageCell" customModule="CapCollector" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="125" height="125"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
|
||||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="125" height="125"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<subviews>
|
|
||||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="dUp-gB-b4C" customClass="RoundedImageView" customModule="CapCollector" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="2" y="2" width="121" height="121"/>
|
|
||||||
</imageView>
|
|
||||||
</subviews>
|
|
||||||
</view>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="dUp-gB-b4C" firstAttribute="leading" secondItem="a3h-Rh-cyS" secondAttribute="leading" constant="2" id="Gc7-h0-pzJ"/>
|
|
||||||
<constraint firstAttribute="bottom" secondItem="dUp-gB-b4C" secondAttribute="bottom" constant="2" id="dKB-wM-BWw"/>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="dUp-gB-b4C" secondAttribute="trailing" constant="2" id="eLS-W2-Owa"/>
|
|
||||||
<constraint firstItem="dUp-gB-b4C" firstAttribute="top" secondItem="a3h-Rh-cyS" secondAttribute="top" constant="2" id="wkf-il-wti"/>
|
|
||||||
</constraints>
|
|
||||||
<connections>
|
|
||||||
<outlet property="capView" destination="dUp-gB-b4C" id="RKW-nR-43M"/>
|
|
||||||
</connections>
|
|
||||||
</collectionViewCell>
|
|
||||||
</cells>
|
|
||||||
</collectionView>
|
|
||||||
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="pHE-7r-I7l" userLabel="Search View">
|
|
||||||
<rect key="frame" x="-1" y="668" width="377" height="49"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" constant="49" id="75Y-Pa-gf4"/>
|
|
||||||
</constraints>
|
|
||||||
</containerView>
|
|
||||||
</subviews>
|
|
||||||
<viewLayoutGuide key="safeArea" id="lmc-ZJ-cgu"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="lmc-ZJ-cgu" firstAttribute="top" secondItem="Vx1-tM-8mD" secondAttribute="top" constant="20" id="9Ai-nd-gYO"/>
|
|
||||||
<constraint firstItem="KLt-lS-0bs" firstAttribute="top" secondItem="Vx1-tM-8mD" secondAttribute="bottom" id="DdH-ID-K5R"/>
|
|
||||||
<constraint firstItem="lmc-ZJ-cgu" firstAttribute="bottom" secondItem="KLt-lS-0bs" secondAttribute="bottom" id="LgP-eI-hGc"/>
|
|
||||||
<constraint firstItem="lmc-ZJ-cgu" firstAttribute="trailing" secondItem="KLt-lS-0bs" secondAttribute="trailing" id="MFg-gH-TZJ"/>
|
|
||||||
<constraint firstItem="KLt-lS-0bs" firstAttribute="leading" secondItem="lmc-ZJ-cgu" secondAttribute="leading" id="MNg-7M-R1B"/>
|
|
||||||
<constraint firstItem="KLt-lS-0bs" firstAttribute="top" secondItem="lmc-ZJ-cgu" secondAttribute="top" id="MgU-8N-zWy"/>
|
|
||||||
<constraint firstItem="lmc-ZJ-cgu" firstAttribute="trailing" secondItem="Vx1-tM-8mD" secondAttribute="trailing" id="PqZ-Gb-7Le"/>
|
|
||||||
<constraint firstItem="Vx1-tM-8mD" firstAttribute="leading" secondItem="lmc-ZJ-cgu" secondAttribute="leading" id="WgW-hG-WpI"/>
|
|
||||||
<constraint firstItem="pHE-7r-I7l" firstAttribute="centerX" secondItem="lmc-ZJ-cgu" secondAttribute="centerX" id="fdQ-KZ-Nj4"/>
|
|
||||||
<constraint firstItem="lmc-ZJ-cgu" firstAttribute="bottom" secondItem="pHE-7r-I7l" secondAttribute="bottom" constant="-50" id="n33-vy-jhn"/>
|
|
||||||
<constraint firstItem="lmc-ZJ-cgu" firstAttribute="trailing" secondItem="pHE-7r-I7l" secondAttribute="trailing" constant="-1" id="sGP-rj-Nsr"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
<tabBarItem key="tabBarItem" systemItem="history" id="Cxk-kX-pY9">
|
|
||||||
<color key="badgeColor" white="0.33333333329999998" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
</tabBarItem>
|
|
||||||
<navigationItem key="navigationItem" title="Main Image" id="Pt2-ad-tUu">
|
|
||||||
<barButtonItem key="backBarButtonItem" title="Back" id="e2a-wC-2Uv"/>
|
|
||||||
</navigationItem>
|
|
||||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
|
||||||
<connections>
|
|
||||||
<outlet property="collection" destination="KLt-lS-0bs" id="Jfu-ZQ-v2b"/>
|
|
||||||
</connections>
|
|
||||||
</viewController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="xnW-hF-IOj" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="2906" y="1150"/>
|
|
||||||
</scene>
|
|
||||||
<!--Caps-->
|
|
||||||
<scene sceneID="SL6-kV-XF2">
|
|
||||||
<objects>
|
|
||||||
<tableViewController id="2ro-5c-16N" customClass="TableView" customModule="CapCollector" customModuleProvider="target" sceneMemberID="viewController">
|
|
||||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="h37-NP-KdK">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<prototypes>
|
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="cap" rowHeight="100" id="B8D-Az-9Y6" customClass="CapCell" customModule="CapCollector" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="0.0" y="44.5" width="375" height="100"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="B8D-Az-9Y6" id="oId-SE-Vio">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="100"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<subviews>
|
|
||||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Z8g-0m-rCx" customClass="RoundedImageView" customModule="CapCollector" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="2" y="2" width="96" height="96"/>
|
|
||||||
<color key="tintColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" secondItem="Z8g-0m-rCx" secondAttribute="height" multiplier="1:1" id="kh6-aY-eca"/>
|
|
||||||
</constraints>
|
|
||||||
<userDefinedRuntimeAttributes>
|
|
||||||
<userDefinedRuntimeAttribute type="color" keyPath="borderColor">
|
|
||||||
<color key="value" systemColor="secondaryLabelColor"/>
|
|
||||||
</userDefinedRuntimeAttribute>
|
|
||||||
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
|
|
||||||
<real key="value" value="1"/>
|
|
||||||
</userDefinedRuntimeAttribute>
|
|
||||||
</userDefinedRuntimeAttributes>
|
|
||||||
</imageView>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="# images" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jmW-Bp-iEs">
|
|
||||||
<rect key="frame" x="106" y="10" width="59" height="17"/>
|
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
|
||||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Some brand with a long name of more than one line" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bRK-Je-2c8">
|
|
||||||
<rect key="frame" x="106" y="29" width="261" height="47"/>
|
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="71 % match" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0S2-Kl-IEy">
|
|
||||||
<rect key="frame" x="292" y="10" width="75" height="17"/>
|
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
|
||||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
</subviews>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="Z8g-0m-rCx" firstAttribute="leading" secondItem="oId-SE-Vio" secondAttribute="leading" constant="2" id="5au-Ct-dWn"/>
|
|
||||||
<constraint firstItem="0S2-Kl-IEy" firstAttribute="trailing" secondItem="bRK-Je-2c8" secondAttribute="trailing" id="A3y-cc-l1F"/>
|
|
||||||
<constraint firstItem="bRK-Je-2c8" firstAttribute="leading" secondItem="jmW-Bp-iEs" secondAttribute="leading" id="FZc-hg-lot"/>
|
|
||||||
<constraint firstItem="0S2-Kl-IEy" firstAttribute="top" secondItem="jmW-Bp-iEs" secondAttribute="top" id="LWs-3Q-FHS"/>
|
|
||||||
<constraint firstItem="bRK-Je-2c8" firstAttribute="top" secondItem="jmW-Bp-iEs" secondAttribute="bottom" constant="2" id="Pfc-yw-wPd"/>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="bRK-Je-2c8" secondAttribute="trailing" constant="8" id="S76-vj-hvt"/>
|
|
||||||
<constraint firstItem="jmW-Bp-iEs" firstAttribute="leading" secondItem="Z8g-0m-rCx" secondAttribute="trailing" constant="8" id="kHN-KC-vU5"/>
|
|
||||||
<constraint firstAttribute="bottom" secondItem="Z8g-0m-rCx" secondAttribute="bottom" constant="2" id="mFR-5k-cDP"/>
|
|
||||||
<constraint firstItem="0S2-Kl-IEy" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="jmW-Bp-iEs" secondAttribute="trailing" constant="10" id="nC9-nB-S66"/>
|
|
||||||
<constraint firstItem="Z8g-0m-rCx" firstAttribute="centerY" secondItem="oId-SE-Vio" secondAttribute="centerY" id="q9Q-78-QRL"/>
|
|
||||||
<constraint firstItem="jmW-Bp-iEs" firstAttribute="top" secondItem="oId-SE-Vio" secondAttribute="top" constant="10" id="yOO-Jh-C4e"/>
|
|
||||||
</constraints>
|
|
||||||
</tableViewCellContentView>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<connections>
|
|
||||||
<outlet property="capImage" destination="Z8g-0m-rCx" id="7vi-yn-4bH"/>
|
|
||||||
<outlet property="countLabel" destination="jmW-Bp-iEs" id="EEH-6Q-T8x"/>
|
|
||||||
<outlet property="matchLabel" destination="0S2-Kl-IEy" id="q1Q-F0-rIZ"/>
|
|
||||||
<outlet property="nameLabel" destination="bRK-Je-2c8" id="bju-xf-e6F"/>
|
|
||||||
</connections>
|
|
||||||
</tableViewCell>
|
|
||||||
</prototypes>
|
|
||||||
<connections>
|
|
||||||
<outlet property="dataSource" destination="2ro-5c-16N" id="fx9-na-Yab"/>
|
|
||||||
<outlet property="delegate" destination="2ro-5c-16N" id="BBl-FL-SqT"/>
|
|
||||||
</connections>
|
|
||||||
</tableView>
|
|
||||||
<navigationItem key="navigationItem" title="Caps" id="qe9-JJ-Ei4">
|
|
||||||
<barButtonItem key="leftBarButtonItem" title="Item" image="arrow.clockwise.icloud" catalog="system" id="SDv-mW-eqq">
|
|
||||||
<connections>
|
|
||||||
<action selector="updateInfo:forEvent:" destination="2ro-5c-16N" id="MDf-gM-ErC"/>
|
|
||||||
</connections>
|
|
||||||
</barButtonItem>
|
|
||||||
<barButtonItem key="rightBarButtonItem" title="Item" image="circle.hexagongrid" catalog="system" id="Bii-kx-Exm">
|
|
||||||
<connections>
|
|
||||||
<action selector="showMosaic:" destination="2ro-5c-16N" id="TSM-H0-ljH"/>
|
|
||||||
</connections>
|
|
||||||
</barButtonItem>
|
|
||||||
</navigationItem>
|
|
||||||
<connections>
|
|
||||||
<outlet property="infoButton" destination="SDv-mW-eqq" id="hKf-A3-chi"/>
|
|
||||||
</connections>
|
|
||||||
</tableViewController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="xpG-Fd-7Lc" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="1654" y="1150"/>
|
|
||||||
</scene>
|
|
||||||
<!--New image-->
|
|
||||||
<scene sceneID="5iQ-Nr-LFc">
|
|
||||||
<objects>
|
|
||||||
<viewController storyboardIdentifier="NewImageController" title="New image" id="oDF-NZ-j9r" customClass="CameraController" customModule="CapCollector" customModuleProvider="target" sceneMemberID="viewController">
|
|
||||||
<view key="view" contentMode="scaleToFill" id="Y3e-A9-cGq">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<subviews>
|
|
||||||
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="75a-Qa-tvd" userLabel="Embedding Camera View">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
|
|
||||||
<subviews>
|
|
||||||
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="JR1-Im-yOB" userLabel="PicturePreview" customClass="CameraView" customModule="CapCollector" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="-80" y="-33" width="535" height="713"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" secondItem="JR1-Im-yOB" secondAttribute="height" multiplier="3:4" id="LL6-Z4-ep1"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
<view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="RWS-jY-sNX" customClass="CropView" customModule="CapCollector" customModuleProvider="target">
|
|
||||||
<rect key="frame" x="-80" y="56" width="535" height="535"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" secondItem="RWS-jY-sNX" secondAttribute="height" multiplier="1:1" id="Buc-aU-mqr"/>
|
|
||||||
</constraints>
|
|
||||||
<userDefinedRuntimeAttributes>
|
|
||||||
<userDefinedRuntimeAttribute type="number" keyPath="relativeSize">
|
|
||||||
<real key="value" value="0.40000000000000002"/>
|
|
||||||
</userDefinedRuntimeAttribute>
|
|
||||||
<userDefinedRuntimeAttribute type="color" keyPath="lineColor">
|
|
||||||
<color key="value" systemColor="systemBlueColor"/>
|
|
||||||
</userDefinedRuntimeAttribute>
|
|
||||||
<userDefinedRuntimeAttribute type="number" keyPath="lineWidth">
|
|
||||||
<real key="value" value="2"/>
|
|
||||||
</userDefinedRuntimeAttribute>
|
|
||||||
</userDefinedRuntimeAttributes>
|
|
||||||
</view>
|
|
||||||
</subviews>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="JR1-Im-yOB" firstAttribute="centerY" secondItem="75a-Qa-tvd" secondAttribute="centerY" id="I0g-m0-LPb"/>
|
|
||||||
<constraint firstItem="JR1-Im-yOB" firstAttribute="centerX" secondItem="75a-Qa-tvd" secondAttribute="centerX" id="PWv-L2-yWL"/>
|
|
||||||
<constraint firstItem="RWS-jY-sNX" firstAttribute="centerY" secondItem="JR1-Im-yOB" secondAttribute="centerY" id="gW5-a3-fBU"/>
|
|
||||||
<constraint firstItem="RWS-jY-sNX" firstAttribute="centerX" secondItem="JR1-Im-yOB" secondAttribute="centerX" id="sxd-Vu-qCf"/>
|
|
||||||
<constraint firstItem="JR1-Im-yOB" firstAttribute="leading" secondItem="75a-Qa-tvd" secondAttribute="leading" constant="-80" id="teJ-k1-aza"/>
|
|
||||||
<constraint firstItem="RWS-jY-sNX" firstAttribute="leading" secondItem="JR1-Im-yOB" secondAttribute="leading" id="xq5-uK-Qgb"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="mGZ-Eq-3Pr" userLabel="Bottom Bar">
|
|
||||||
<rect key="frame" x="0.0" y="567" width="375" height="80"/>
|
|
||||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="Q93-lY-5IF">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
</view>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" constant="80" id="Mw4-4x-jsf"/>
|
|
||||||
</constraints>
|
|
||||||
<blurEffect style="regular"/>
|
|
||||||
</visualEffectView>
|
|
||||||
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="249" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Vaw-Dn-P4X">
|
|
||||||
<rect key="frame" x="147.5" y="567" width="80" height="80"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" secondItem="Vaw-Dn-P4X" secondAttribute="height" multiplier="1:1" id="dOA-ls-qFN"/>
|
|
||||||
</constraints>
|
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
|
||||||
<state key="normal" image="camera" catalog="system">
|
|
||||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="large" weight="regular"/>
|
|
||||||
</state>
|
|
||||||
<state key="disabled">
|
|
||||||
<color key="titleColor" white="0.33333333329999998" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
</state>
|
|
||||||
<state key="highlighted">
|
|
||||||
<color key="titleColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
</state>
|
|
||||||
<connections>
|
|
||||||
<action selector="imageButtonPressed" destination="oDF-NZ-j9r" eventType="touchUpInside" id="101-8q-Ys4"/>
|
|
||||||
</connections>
|
|
||||||
</button>
|
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="mcC-De-ygo">
|
|
||||||
<rect key="frame" x="295" y="567" width="80" height="80"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" secondItem="mcC-De-ygo" secondAttribute="height" multiplier="1:1" id="sCS-gR-a47"/>
|
|
||||||
</constraints>
|
|
||||||
<state key="normal" image="xmark" catalog="system">
|
|
||||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="large"/>
|
|
||||||
</state>
|
|
||||||
<connections>
|
|
||||||
<action selector="backButtonPressed" destination="oDF-NZ-j9r" eventType="touchUpInside" id="ZKP-ty-JVi"/>
|
|
||||||
</connections>
|
|
||||||
</button>
|
|
||||||
</subviews>
|
|
||||||
<viewLayoutGuide key="safeArea" id="oi0-LU-PyW"/>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="75a-Qa-tvd" firstAttribute="leading" secondItem="oi0-LU-PyW" secondAttribute="leading" id="3sA-U9-YPf"/>
|
|
||||||
<constraint firstItem="oi0-LU-PyW" firstAttribute="bottom" secondItem="75a-Qa-tvd" secondAttribute="bottom" id="44I-wq-3q0"/>
|
|
||||||
<constraint firstItem="mGZ-Eq-3Pr" firstAttribute="leading" secondItem="oi0-LU-PyW" secondAttribute="leading" id="6KA-j9-M2J"/>
|
|
||||||
<constraint firstItem="Vaw-Dn-P4X" firstAttribute="bottom" secondItem="mGZ-Eq-3Pr" secondAttribute="bottom" id="6MT-0x-PCE"/>
|
|
||||||
<constraint firstItem="Vaw-Dn-P4X" firstAttribute="centerX" secondItem="oi0-LU-PyW" secondAttribute="centerX" id="baa-SQ-aA8"/>
|
|
||||||
<constraint firstItem="mcC-De-ygo" firstAttribute="bottom" secondItem="mGZ-Eq-3Pr" secondAttribute="bottom" id="dpX-8L-TKB"/>
|
|
||||||
<constraint firstItem="mcC-De-ygo" firstAttribute="top" secondItem="mGZ-Eq-3Pr" secondAttribute="top" id="e4q-pi-Iby"/>
|
|
||||||
<constraint firstItem="mcC-De-ygo" firstAttribute="trailing" secondItem="mGZ-Eq-3Pr" secondAttribute="trailing" id="eOt-AO-Kwc"/>
|
|
||||||
<constraint firstItem="oi0-LU-PyW" firstAttribute="trailing" secondItem="mGZ-Eq-3Pr" secondAttribute="trailing" id="fq6-bZ-kzu"/>
|
|
||||||
<constraint firstItem="Vaw-Dn-P4X" firstAttribute="top" secondItem="mGZ-Eq-3Pr" secondAttribute="top" id="nSi-IN-buo"/>
|
|
||||||
<constraint firstItem="75a-Qa-tvd" firstAttribute="top" secondItem="oi0-LU-PyW" secondAttribute="top" id="uOs-bC-Ham"/>
|
|
||||||
<constraint firstItem="oi0-LU-PyW" firstAttribute="bottom" secondItem="mGZ-Eq-3Pr" secondAttribute="bottom" id="vRz-6U-FKY"/>
|
|
||||||
<constraint firstItem="oi0-LU-PyW" firstAttribute="trailing" secondItem="75a-Qa-tvd" secondAttribute="trailing" id="wnh-xb-MEk"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
<tabBarItem key="tabBarItem" systemItem="search" id="eNS-Ka-b0t"/>
|
|
||||||
<modalPageSheetSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
|
||||||
<connections>
|
|
||||||
<outlet property="cameraView" destination="JR1-Im-yOB" id="qau-yi-xCB"/>
|
|
||||||
<outlet property="cancelButton" destination="mcC-De-ygo" id="Rdr-gp-sDT"/>
|
|
||||||
<outlet property="cropView" destination="RWS-jY-sNX" id="9OR-8z-a1p"/>
|
|
||||||
<outlet property="imageButton" destination="Vaw-Dn-P4X" id="2mY-wk-jWZ"/>
|
|
||||||
</connections>
|
|
||||||
</viewController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="u9k-DO-1HN" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="2281" y="1150"/>
|
|
||||||
</scene>
|
|
||||||
<!--Grid View Controller-->
|
|
||||||
<scene sceneID="Hjr-cD-s35">
|
|
||||||
<objects>
|
|
||||||
<viewController storyboardIdentifier="GridView" id="wzG-WL-mtW" customClass="GridViewController" customModule="CapCollector" customModuleProvider="target" sceneMemberID="viewController">
|
|
||||||
<view key="view" contentMode="scaleToFill" id="9sc-l8-fcK">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<subviews>
|
|
||||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TCx-cV-mMG">
|
|
||||||
<rect key="frame" x="0.0" y="44" width="375" height="623"/>
|
|
||||||
</scrollView>
|
|
||||||
</subviews>
|
|
||||||
<viewLayoutGuide key="safeArea" id="WAE-if-wuA"/>
|
|
||||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="TCx-cV-mMG" firstAttribute="top" secondItem="WAE-if-wuA" secondAttribute="top" id="C5g-UD-eI2"/>
|
|
||||||
<constraint firstItem="WAE-if-wuA" firstAttribute="bottom" secondItem="TCx-cV-mMG" secondAttribute="bottom" id="J5D-Xs-Unl"/>
|
|
||||||
<constraint firstItem="TCx-cV-mMG" firstAttribute="leading" secondItem="WAE-if-wuA" secondAttribute="leading" id="fXw-7M-B0G"/>
|
|
||||||
<constraint firstItem="WAE-if-wuA" firstAttribute="trailing" secondItem="TCx-cV-mMG" secondAttribute="trailing" id="sY8-ka-loB"/>
|
|
||||||
</constraints>
|
|
||||||
</view>
|
|
||||||
<navigationItem key="navigationItem" id="bdd-GD-zBH">
|
|
||||||
<barButtonItem key="rightBarButtonItem" title="Colors" id="0qm-Vt-S3s">
|
|
||||||
<connections>
|
|
||||||
<action selector="toggleAverageColor:" destination="wzG-WL-mtW" id="EhG-8E-FcQ"/>
|
|
||||||
</connections>
|
|
||||||
</barButtonItem>
|
|
||||||
</navigationItem>
|
|
||||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
|
||||||
<connections>
|
|
||||||
<outlet property="scrollView" destination="TCx-cV-mMG" id="Isn-jV-DBf"/>
|
|
||||||
</connections>
|
|
||||||
</viewController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="9Os-vj-mkT" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="3526" y="1150"/>
|
|
||||||
</scene>
|
|
||||||
<!--Sort Controller-->
|
|
||||||
<scene sceneID="fFw-OX-Mag">
|
|
||||||
<objects>
|
|
||||||
<tableViewController storyboardIdentifier="SortController" id="xVJ-JZ-U8g" customClass="SortController" customModule="CapCollector" customModuleProvider="target" sceneMemberID="viewController">
|
|
||||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" scrollEnabled="NO" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="LcF-G6-Sln">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="150" height="310"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
|
|
||||||
<prototypes>
|
|
||||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" reuseIdentifier="SortCell" textLabel="2cU-Pz-MYZ" rowHeight="40" style="IBUITableViewCellStyleDefault" id="vYb-0s-NQp">
|
|
||||||
<rect key="frame" x="0.0" y="49.5" width="150" height="40"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="vYb-0s-NQp" id="8bD-vd-HWF">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="113.5" height="40"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<subviews>
|
|
||||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Ascending" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="2cU-Pz-MYZ">
|
|
||||||
<rect key="frame" x="16" y="0.0" width="89.5" height="40"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
|
||||||
<nil key="textColor"/>
|
|
||||||
<nil key="highlightedColor"/>
|
|
||||||
</label>
|
|
||||||
</subviews>
|
|
||||||
</tableViewCellContentView>
|
|
||||||
<color key="backgroundColor" systemColor="tableCellGroupedBackgroundColor"/>
|
|
||||||
</tableViewCell>
|
|
||||||
</prototypes>
|
|
||||||
<sections/>
|
|
||||||
<connections>
|
|
||||||
<outlet property="dataSource" destination="xVJ-JZ-U8g" id="27V-wO-lvY"/>
|
|
||||||
<outlet property="delegate" destination="xVJ-JZ-U8g" id="TdR-BV-8pz"/>
|
|
||||||
</connections>
|
|
||||||
</tableView>
|
|
||||||
<nil key="simulatedTopBarMetrics"/>
|
|
||||||
<nil key="simulatedBottomBarMetrics"/>
|
|
||||||
<size key="freeformSize" width="150" height="310"/>
|
|
||||||
</tableViewController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="l35-YK-LmX" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="1834" y="644"/>
|
|
||||||
</scene>
|
|
||||||
<!--Navigation Controller-->
|
|
||||||
<scene sceneID="cDZ-9F-oGg">
|
|
||||||
<objects>
|
|
||||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="qlf-I7-aOI" customClass="NavigationController" customModule="CapCollector" customModuleProvider="target" sceneMemberID="viewController">
|
|
||||||
<toolbarItems/>
|
|
||||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="tO3-6d-IEo">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
|
||||||
</navigationBar>
|
|
||||||
<nil name="viewControllers"/>
|
|
||||||
<connections>
|
|
||||||
<segue destination="2ro-5c-16N" kind="relationship" relationship="rootViewController" id="il4-pa-RJf"/>
|
|
||||||
</connections>
|
|
||||||
</navigationController>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="ar3-Vx-90J" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
|
||||||
</objects>
|
|
||||||
<point key="canvasLocation" x="985" y="1150"/>
|
|
||||||
</scene>
|
|
||||||
</scenes>
|
|
||||||
<resources>
|
|
||||||
<image name="arrow.clockwise.icloud" catalog="system" width="128" height="88"/>
|
|
||||||
<image name="camera" catalog="system" width="128" height="94"/>
|
|
||||||
<image name="circle.hexagongrid" catalog="system" width="128" height="112"/>
|
|
||||||
<image name="xmark" catalog="system" width="128" height="113"/>
|
|
||||||
<systemColor name="secondaryLabelColor">
|
|
||||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
</systemColor>
|
|
||||||
<systemColor name="secondarySystemBackgroundColor">
|
|
||||||
<color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
</systemColor>
|
|
||||||
<systemColor name="systemBackgroundColor">
|
|
||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
</systemColor>
|
|
||||||
<systemColor name="systemBlueColor">
|
|
||||||
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
</systemColor>
|
|
||||||
<systemColor name="tableCellGroupedBackgroundColor">
|
|
||||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
</systemColor>
|
|
||||||
</resources>
|
|
||||||
</document>
|
|
32
Caps/Camera/CameraError.swift
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum CameraError: Error {
|
||||||
|
case cameraUnavailable
|
||||||
|
case cannotAddInput
|
||||||
|
case cannotAddOutput
|
||||||
|
case createCaptureInput(Error)
|
||||||
|
case deniedAuthorization
|
||||||
|
case restrictedAuthorization
|
||||||
|
case unknownAuthorization
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CameraError: LocalizedError {
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .cameraUnavailable:
|
||||||
|
return "Camera unavailable"
|
||||||
|
case .cannotAddInput:
|
||||||
|
return "Cannot add capture input to session"
|
||||||
|
case .cannotAddOutput:
|
||||||
|
return "Cannot add video output to session"
|
||||||
|
case .createCaptureInput(let error):
|
||||||
|
return "Creating capture input for camera: \(error.localizedDescription)"
|
||||||
|
case .deniedAuthorization:
|
||||||
|
return "Camera access denied"
|
||||||
|
case .restrictedAuthorization:
|
||||||
|
return "Attempting to access a restricted capture device"
|
||||||
|
case .unknownAuthorization:
|
||||||
|
return "Unknown authorization status for capture device"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
172
Caps/Camera/CameraManager.swift
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import Foundation
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
class CameraManager: ObservableObject {
|
||||||
|
enum Status {
|
||||||
|
case unconfigured
|
||||||
|
case configured
|
||||||
|
case unauthorized
|
||||||
|
case failed
|
||||||
|
}
|
||||||
|
|
||||||
|
static let shared = CameraManager()
|
||||||
|
|
||||||
|
@Published var error: CameraError?
|
||||||
|
|
||||||
|
let session = AVCaptureSession()
|
||||||
|
|
||||||
|
private let sessionQueue = DispatchQueue(label: "de.christophhagen.cam")
|
||||||
|
private let videoOutput = AVCaptureVideoDataOutput()
|
||||||
|
private let photoOutput = AVCapturePhotoOutput()
|
||||||
|
private var status = Status.unconfigured
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func set(error: CameraError?) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.error = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func checkPermissions() {
|
||||||
|
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
||||||
|
case .notDetermined:
|
||||||
|
sessionQueue.suspend()
|
||||||
|
AVCaptureDevice.requestAccess(for: .video) { authorized in
|
||||||
|
if !authorized {
|
||||||
|
self.status = .unauthorized
|
||||||
|
self.set(error: .deniedAuthorization)
|
||||||
|
}
|
||||||
|
self.sessionQueue.resume()
|
||||||
|
}
|
||||||
|
case .restricted:
|
||||||
|
status = .unauthorized
|
||||||
|
set(error: .restrictedAuthorization)
|
||||||
|
case .denied:
|
||||||
|
status = .unauthorized
|
||||||
|
set(error: .deniedAuthorization)
|
||||||
|
case .authorized:
|
||||||
|
break
|
||||||
|
@unknown default:
|
||||||
|
status = .unauthorized
|
||||||
|
set(error: .unknownAuthorization)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureCaptureSession() {
|
||||||
|
guard status == .unconfigured else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.beginConfiguration()
|
||||||
|
session.sessionPreset = .photo
|
||||||
|
|
||||||
|
defer {
|
||||||
|
session.commitConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
let device = AVCaptureDevice.default(
|
||||||
|
.builtInWideAngleCamera,
|
||||||
|
for: .video,
|
||||||
|
position: .back)
|
||||||
|
guard let camera = device else {
|
||||||
|
set(error: .cameraUnavailable)
|
||||||
|
status = .failed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let cameraInput: AVCaptureDeviceInput
|
||||||
|
do {
|
||||||
|
cameraInput = try AVCaptureDeviceInput(device: camera)
|
||||||
|
} catch {
|
||||||
|
set(error: .createCaptureInput(error))
|
||||||
|
status = .failed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard session.canAddInput(cameraInput) else {
|
||||||
|
set(error: .cannotAddInput)
|
||||||
|
status = .failed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session.addInput(cameraInput)
|
||||||
|
|
||||||
|
|
||||||
|
if session.canAddOutput(videoOutput) {
|
||||||
|
session.addOutput(videoOutput)
|
||||||
|
|
||||||
|
videoOutput.videoSettings =
|
||||||
|
[kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA]
|
||||||
|
|
||||||
|
let videoConnection = videoOutput.connection(with: .video)
|
||||||
|
videoConnection?.videoOrientation = .portrait
|
||||||
|
} else {
|
||||||
|
set(error: .cannotAddOutput)
|
||||||
|
status = .failed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard session.canAddOutput(photoOutput) else {
|
||||||
|
set(error: .cannotAddOutput)
|
||||||
|
status = .failed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session.addOutput(photoOutput)
|
||||||
|
photoOutput.isHighResolutionCaptureEnabled = true
|
||||||
|
photoOutput.isDepthDataDeliveryEnabled = false
|
||||||
|
photoOutput.isLivePhotoCaptureEnabled = false
|
||||||
|
|
||||||
|
status = .configured
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configure() {
|
||||||
|
checkPermissions()
|
||||||
|
|
||||||
|
sessionQueue.async {
|
||||||
|
self.configureCaptureSession()
|
||||||
|
self.session.startRunning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setVideoDelegate(_ delegate: AVCaptureVideoDataOutputSampleBufferDelegate,
|
||||||
|
queue: DispatchQueue) {
|
||||||
|
sessionQueue.async {
|
||||||
|
self.videoOutput.setSampleBufferDelegate(delegate, queue: queue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopVideoCaptureSession() {
|
||||||
|
sessionQueue.async {
|
||||||
|
guard self.status == .configured else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard self.session.isRunning else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.session.stopRunning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startVideoCapture() {
|
||||||
|
guard status == .configured else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sessionQueue.async {
|
||||||
|
guard !self.session.isRunning else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.session.startRunning()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Photo Capture
|
||||||
|
|
||||||
|
func capturePhoto(delegate: AVCapturePhotoCaptureDelegate) {
|
||||||
|
sessionQueue.async {
|
||||||
|
let photoSettings = AVCapturePhotoSettings()
|
||||||
|
photoSettings.flashMode = .off
|
||||||
|
self.photoOutput.capturePhoto(with: photoSettings, delegate: delegate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
127
Caps/Camera/CameraView.swift
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
struct CameraView: View {
|
||||||
|
|
||||||
|
static let cameraImagePadding: CGFloat = 300
|
||||||
|
|
||||||
|
private static let circleSize: CGFloat = 180
|
||||||
|
private var circleSize: CGFloat {
|
||||||
|
CameraView.circleSize
|
||||||
|
}
|
||||||
|
|
||||||
|
static var circleCropFactor: CGFloat {
|
||||||
|
let fullWidth = UIScreen.main.bounds.width + 2 * cameraImagePadding
|
||||||
|
return circleSize / fullWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
private let circleStrength: CGFloat = 3
|
||||||
|
|
||||||
|
private let circleColor: Color = .green
|
||||||
|
|
||||||
|
private let captureButtonSize: CGFloat = 110
|
||||||
|
private let captureButtonHeight: CGFloat = 40
|
||||||
|
private let captureButtonWidth: CGFloat = 50
|
||||||
|
|
||||||
|
private let cancelButtonSize: CGFloat = 75
|
||||||
|
private let cancelIconSize: CGFloat = 25
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var isPresented: Bool
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var image: UIImage?
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var capId: Int?
|
||||||
|
|
||||||
|
@StateObject
|
||||||
|
private var model = ContentViewModel()
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
var database: Database
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
FrameView(image: model.frame)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
.padding(-CameraView.cameraImagePadding)
|
||||||
|
|
||||||
|
ErrorView(error: model.error)
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button(action: dismiss) {
|
||||||
|
Image(systemSymbol: .xmark)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: cancelIconSize, height: cancelIconSize)
|
||||||
|
.padding((cancelButtonSize-cancelIconSize)/2)
|
||||||
|
.background(.thinMaterial)
|
||||||
|
.cornerRadius(cancelButtonSize/2)
|
||||||
|
}.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button(action: capture) {
|
||||||
|
Image(systemSymbol: .camera)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: captureButtonWidth, height: captureButtonHeight)
|
||||||
|
.padding(.horizontal, (captureButtonSize - captureButtonWidth)/2)
|
||||||
|
.padding(.vertical, (captureButtonSize - captureButtonHeight)/2)
|
||||||
|
.background(.thinMaterial)
|
||||||
|
.cornerRadius(captureButtonSize / 2)
|
||||||
|
}.padding()
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text("")
|
||||||
|
.frame(width: circleSize, height: circleSize, alignment: .center)
|
||||||
|
.overlay(RoundedRectangle(cornerRadius: circleSize/2)
|
||||||
|
.stroke(lineWidth: circleStrength)
|
||||||
|
.foregroundColor(circleColor))
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}.ignoresSafeArea()
|
||||||
|
|
||||||
|
}
|
||||||
|
.onAppear() {
|
||||||
|
model.startCapture()
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
model.endCapture()
|
||||||
|
}.onChange(of: model.image) { image in
|
||||||
|
if let capId = capId, let image = image {
|
||||||
|
database.save(image, for: capId)
|
||||||
|
} else {
|
||||||
|
database.image = image
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func dismiss() {
|
||||||
|
isPresented = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func capture() {
|
||||||
|
model.captureImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CameraView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
CameraView(isPresented: .constant(true),
|
||||||
|
image: .constant(nil),
|
||||||
|
capId: .constant(nil))
|
||||||
|
}
|
||||||
|
}
|
57
Caps/Camera/ContentViewModel.swift
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import CoreImage
|
||||||
|
import AVFoundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class ContentViewModel: ObservableObject {
|
||||||
|
|
||||||
|
@Published var error: Error?
|
||||||
|
@Published var frame: CGImage?
|
||||||
|
@Published var image: UIImage?
|
||||||
|
|
||||||
|
private let context = CIContext()
|
||||||
|
|
||||||
|
private let cameraManager = CameraManager.shared
|
||||||
|
private let frameManager = FrameManager.shared
|
||||||
|
|
||||||
|
init() {
|
||||||
|
setupSubscriptions()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupSubscriptions() {
|
||||||
|
frameManager.image = nil
|
||||||
|
frameManager.current = nil
|
||||||
|
|
||||||
|
cameraManager.$error
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
|
.map { $0 }
|
||||||
|
.assign(to: &$error)
|
||||||
|
|
||||||
|
frameManager.$current
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
|
.compactMap { buffer in
|
||||||
|
guard let image = CGImage.create(from: buffer) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let ciImage = CIImage(cgImage: image)
|
||||||
|
return self.context.createCGImage(ciImage, from: ciImage.extent)
|
||||||
|
}
|
||||||
|
.assign(to: &$frame)
|
||||||
|
|
||||||
|
frameManager.$image
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
|
.assign(to: &$image)
|
||||||
|
}
|
||||||
|
|
||||||
|
func endCapture() {
|
||||||
|
cameraManager.stopVideoCaptureSession()
|
||||||
|
}
|
||||||
|
|
||||||
|
func startCapture() {
|
||||||
|
cameraManager.startVideoCapture()
|
||||||
|
}
|
||||||
|
|
||||||
|
func captureImage() {
|
||||||
|
cameraManager.capturePhoto(delegate: frameManager)
|
||||||
|
}
|
||||||
|
}
|
27
Caps/Camera/ErrorView.swift
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ErrorView: View {
|
||||||
|
var error: Error?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
Text(error?.localizedDescription ?? "")
|
||||||
|
.bold()
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(8)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.background(Color.red.edgesIgnoringSafeArea(.top))
|
||||||
|
.opacity(error == nil ? 0.0 : 1.0)
|
||||||
|
.animation(.easeInOut, value: 0.25)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ErrorView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ErrorView(error: CameraError.cannotAddInput)
|
||||||
|
}
|
||||||
|
}
|
70
Caps/Camera/FrameManager.swift
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import AVFoundation
|
||||||
|
import CoreGraphics
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class FrameManager: NSObject, ObservableObject {
|
||||||
|
|
||||||
|
static let shared = FrameManager()
|
||||||
|
|
||||||
|
@Published var current: CVPixelBuffer?
|
||||||
|
|
||||||
|
@Published var image: UIImage?
|
||||||
|
|
||||||
|
let videoOutputQueue = DispatchQueue(
|
||||||
|
label: "de.christophhagen.videoout",
|
||||||
|
qos: .userInitiated,
|
||||||
|
attributes: [],
|
||||||
|
autoreleaseFrequency: .workItem)
|
||||||
|
|
||||||
|
private override init() {
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
CameraManager.shared.setVideoDelegate(self, queue: videoOutputQueue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FrameManager: AVCaptureVideoDataOutputSampleBufferDelegate {
|
||||||
|
|
||||||
|
func captureOutput(
|
||||||
|
_ output: AVCaptureOutput,
|
||||||
|
didOutput sampleBuffer: CMSampleBuffer,
|
||||||
|
from connection: AVCaptureConnection
|
||||||
|
) {
|
||||||
|
if let buffer = sampleBuffer.imageBuffer {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.current = buffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FrameManager: AVCapturePhotoCaptureDelegate {
|
||||||
|
|
||||||
|
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
|
||||||
|
let image = convert(photo, error: error)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func convert(_ photo: AVCapturePhoto, error: Error?) -> UIImage? {
|
||||||
|
guard error == nil else {
|
||||||
|
log("PhotoCaptureHandler: \(error!)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let cgImage = photo.cgImageRepresentation() else {
|
||||||
|
log("PhotoCaptureHandler: No image captured")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let image = UIImage(cgImage: cgImage, scale: UIScreen.main.scale, orientation: .right)
|
||||||
|
guard let masked = image.crop(factor: CameraView.circleCropFactor).circleMasked else {
|
||||||
|
log("Could not mask image")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
print(image.size)
|
||||||
|
print(masked.size)
|
||||||
|
print(masked.scale)
|
||||||
|
return masked
|
||||||
|
}
|
||||||
|
}
|
30
Caps/Camera/FrameView.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FrameView: View {
|
||||||
|
var image: CGImage?
|
||||||
|
|
||||||
|
private let label = Text("Video feed")
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if let image = image {
|
||||||
|
GeometryReader { geometry in
|
||||||
|
Image(image, scale: 1.0, orientation: .up, label: label)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(
|
||||||
|
width: geometry.size.width,
|
||||||
|
height: geometry.size.height,
|
||||||
|
alignment: .center)
|
||||||
|
.clipped()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FrameView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
FrameView(image: nil)
|
||||||
|
}
|
||||||
|
}
|
18
Caps/CapsApp.swift
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
#warning("TODO: Create new caps")
|
||||||
|
#warning("TODO: Add colors")
|
||||||
|
#warning("TODO: Grid view")
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct CapsApp: App {
|
||||||
|
|
||||||
|
let database = Database(server: URL(string: "https://christophhagen.de/caps")!)
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
ContentView()
|
||||||
|
.environmentObject(database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,146 +0,0 @@
|
|||||||
//
|
|
||||||
// CameraController.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 22.02.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
protocol CameraControllerDelegate {
|
|
||||||
|
|
||||||
func didCapture(image: UIImage)
|
|
||||||
|
|
||||||
func didCancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
class CameraController: UIViewController {
|
|
||||||
|
|
||||||
// MARK: Outlets
|
|
||||||
|
|
||||||
@IBOutlet weak var imageButton: UIButton!
|
|
||||||
|
|
||||||
@IBOutlet weak var cropView: CropView!
|
|
||||||
|
|
||||||
@IBOutlet weak var cancelButton: UIButton!
|
|
||||||
|
|
||||||
@IBOutlet weak var cameraView: CameraView! {
|
|
||||||
didSet {
|
|
||||||
cameraView.configure()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var delegate: CameraControllerDelegate?
|
|
||||||
|
|
||||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
|
||||||
return .portrait
|
|
||||||
}
|
|
||||||
|
|
||||||
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
|
|
||||||
return .portrait
|
|
||||||
}
|
|
||||||
|
|
||||||
override var shouldAutorotate: Bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Actions
|
|
||||||
|
|
||||||
@IBAction func backButtonPressed() {
|
|
||||||
self.giveFeedback(.medium)
|
|
||||||
delegate?.didCancel()
|
|
||||||
self.dismiss(animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func imageButtonPressed() {
|
|
||||||
self.giveFeedback(.medium)
|
|
||||||
imageButton.isEnabled = false
|
|
||||||
cameraView.capture()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Life cycle
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
|
||||||
super.viewWillAppear(animated)
|
|
||||||
|
|
||||||
cameraView.delegate = self
|
|
||||||
|
|
||||||
cameraView.launch { success, error in
|
|
||||||
guard let err = error else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch err {
|
|
||||||
case "No camera access": self.showNoCameraAccessAlert()
|
|
||||||
case "Camera error": self.showAlert("Unable to capture media")
|
|
||||||
default: self.showAlert("Error in camera setup")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.imageButton.isEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
|
||||||
super.viewWillDisappear(animated)
|
|
||||||
cameraView.complete()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func giveFeedback(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
|
|
||||||
let generator = UIImpactFeedbackGenerator(style: style)
|
|
||||||
generator.impactOccurred()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Alerts
|
|
||||||
|
|
||||||
private func showNoCameraAccessAlert() {
|
|
||||||
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.",
|
|
||||||
preferredStyle: .alert)//,
|
|
||||||
//blurStyle: .dark)
|
|
||||||
|
|
||||||
let okAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
|
|
||||||
alert.addAction(okAction)
|
|
||||||
|
|
||||||
let settingsAction = UIAlertAction(title: "Settings", style: .default, handler: { _ in
|
|
||||||
// Take the user to Settings app to possibly change permission.
|
|
||||||
guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { return }
|
|
||||||
if UIApplication.shared.canOpenURL(settingsUrl) {
|
|
||||||
UIApplication.shared.open(settingsUrl, completionHandler: nil)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
alert.addAction(settingsAction)
|
|
||||||
self.present(alert, animated: true, completion: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
|
|
||||||
cameraView.didReceiveTouch(touches.first!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension CameraController: PhotoCaptureHandlerDelegate {
|
|
||||||
|
|
||||||
func didCapture(_ image: UIImage?) {
|
|
||||||
log("Image captured")
|
|
||||||
let factor = CGFloat(cropView.relativeSize)
|
|
||||||
self.dismiss(animated: true)
|
|
||||||
guard let img = image else {
|
|
||||||
self.error("No image captured")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard delegate != nil else {
|
|
||||||
self.error("No delegate")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let masked = img.crop(factor: factor).circleMasked else {
|
|
||||||
self.error("Could not mask image")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let scaled = masked.resize(to: Cap.imageSize)
|
|
||||||
|
|
||||||
delegate!.didCapture(image: scaled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension CameraController: Logger { }
|
|
@ -1,224 +0,0 @@
|
|||||||
//
|
|
||||||
// CameraView.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 07.02.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import AVFoundation
|
|
||||||
|
|
||||||
class CameraView: UIView {
|
|
||||||
|
|
||||||
// MARK: UIView overrides
|
|
||||||
|
|
||||||
/**
|
|
||||||
Override for AVCapture
|
|
||||||
*/
|
|
||||||
override class var layerClass: AnyClass {
|
|
||||||
return AVCaptureVideoPreviewLayer.self
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Enums
|
|
||||||
|
|
||||||
private enum SessionSetupResult {
|
|
||||||
case success
|
|
||||||
case notAuthorized
|
|
||||||
case configurationFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Variables
|
|
||||||
|
|
||||||
var delegate: PhotoCaptureHandlerDelegate? {
|
|
||||||
get {
|
|
||||||
return photoCaptureProcessor.delegate
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
photoCaptureProcessor.delegate = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private let session = AVCaptureSession()
|
|
||||||
|
|
||||||
private var isSessionRunning = false
|
|
||||||
|
|
||||||
/// Communicate with the session and other session objects on this queue.
|
|
||||||
private let sessionQueue = DispatchQueue(label: "session queue")
|
|
||||||
|
|
||||||
private var setupResult: SessionSetupResult = .success
|
|
||||||
|
|
||||||
var videoDeviceInput: AVCaptureDeviceInput!
|
|
||||||
|
|
||||||
private let photoOutput = AVCapturePhotoOutput()
|
|
||||||
|
|
||||||
private var cameraDevice: AVCaptureDevice?
|
|
||||||
|
|
||||||
private let photoCaptureProcessor = PhotoCaptureHandler()
|
|
||||||
|
|
||||||
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
|
|
||||||
return layer as! AVCaptureVideoPreviewLayer
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Life cycle
|
|
||||||
|
|
||||||
func configure() {
|
|
||||||
videoPreviewLayer.session = session
|
|
||||||
|
|
||||||
checkPermission()
|
|
||||||
|
|
||||||
// Setup the capture session.
|
|
||||||
sessionQueue.async {
|
|
||||||
self.configureSession()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func launch(completionHandler: @escaping (Bool, String?) -> ()) {
|
|
||||||
sessionQueue.async {
|
|
||||||
switch self.setupResult {
|
|
||||||
case .success:
|
|
||||||
// Only setup observers and start the session running if setup succeeded.
|
|
||||||
self.session.startRunning()
|
|
||||||
self.isSessionRunning = self.session.isRunning
|
|
||||||
|
|
||||||
case .notAuthorized:
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
completionHandler(false, "No camera access")
|
|
||||||
}
|
|
||||||
|
|
||||||
case .configurationFailed:
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
completionHandler(false, "Camera error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func complete() {
|
|
||||||
sessionQueue.async {
|
|
||||||
if self.setupResult == .success {
|
|
||||||
self.session.stopRunning()
|
|
||||||
self.isSessionRunning = self.session.isRunning
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Photo Capture
|
|
||||||
|
|
||||||
func capture() {
|
|
||||||
sessionQueue.async {
|
|
||||||
self.photoOutput.capturePhoto(
|
|
||||||
with: self.photoCaptureProcessor.photoSettings,
|
|
||||||
delegate: self.photoCaptureProcessor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Camera permissions
|
|
||||||
|
|
||||||
private func checkPermission() {
|
|
||||||
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
|
||||||
case .authorized: break
|
|
||||||
case .notDetermined:
|
|
||||||
sessionQueue.suspend()
|
|
||||||
AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
|
|
||||||
if !granted {
|
|
||||||
self.setupResult = .notAuthorized
|
|
||||||
}
|
|
||||||
self.sessionQueue.resume()
|
|
||||||
})
|
|
||||||
|
|
||||||
default:
|
|
||||||
// The user has previously denied access.
|
|
||||||
setupResult = .notAuthorized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call this on the session queue.
|
|
||||||
private func configureSession() {
|
|
||||||
if setupResult != .success {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
session.beginConfiguration()
|
|
||||||
|
|
||||||
/*
|
|
||||||
We do not create an AVCaptureMovieFileOutput when setting up the session because the
|
|
||||||
AVCaptureMovieFileOutput does not support movie recording with AVCaptureSession.Preset.Photo.
|
|
||||||
*/
|
|
||||||
session.sessionPreset = .photo
|
|
||||||
|
|
||||||
// Add video input.
|
|
||||||
|
|
||||||
guard let backCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else {
|
|
||||||
error("No camera on device")
|
|
||||||
setupResult = .configurationFailed
|
|
||||||
session.commitConfiguration()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let videoDeviceInput: AVCaptureDeviceInput
|
|
||||||
do {
|
|
||||||
videoDeviceInput = try AVCaptureDeviceInput(device: backCameraDevice)
|
|
||||||
} catch {
|
|
||||||
self.error("Could not create video device input: \(error)")
|
|
||||||
setupResult = .configurationFailed
|
|
||||||
session.commitConfiguration()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard session.canAddInput(videoDeviceInput) else {
|
|
||||||
error("Could not add video device input to the session")
|
|
||||||
setupResult = .configurationFailed
|
|
||||||
session.commitConfiguration()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
session.addInput(videoDeviceInput)
|
|
||||||
self.videoDeviceInput = videoDeviceInput
|
|
||||||
self.cameraDevice = backCameraDevice
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.videoPreviewLayer.connection?.videoOrientation = .portrait
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add photo output.
|
|
||||||
guard session.canAddOutput(photoOutput) else {
|
|
||||||
error("Could not add photo output to the session")
|
|
||||||
setupResult = .configurationFailed
|
|
||||||
session.commitConfiguration()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
session.addOutput(photoOutput)
|
|
||||||
|
|
||||||
photoOutput.isHighResolutionCaptureEnabled = true
|
|
||||||
photoOutput.isDepthDataDeliveryEnabled = false
|
|
||||||
//photoOutput.isDualCameraDualPhotoDeliveryEnabled = false
|
|
||||||
photoOutput.isLivePhotoCaptureEnabled = false
|
|
||||||
session.commitConfiguration()
|
|
||||||
}
|
|
||||||
|
|
||||||
func didReceiveTouch(_ touch: UITouch) {
|
|
||||||
let screenSize = bounds.size
|
|
||||||
let location = touch.location(in: self)
|
|
||||||
let focusPoint = CGPoint(x: location.y / screenSize.height, y: 1.0 - location.x / screenSize.width)
|
|
||||||
log("Focusing on point (\(focusPoint.x),\(focusPoint.y))")
|
|
||||||
if let device = cameraDevice {
|
|
||||||
do {
|
|
||||||
try device.lockForConfiguration()
|
|
||||||
|
|
||||||
if device.isFocusPointOfInterestSupported {
|
|
||||||
device.focusPointOfInterest = focusPoint
|
|
||||||
device.focusMode = .autoFocus
|
|
||||||
}
|
|
||||||
// if device.isExposurePointOfInterestSupported {
|
|
||||||
// device.exposurePointOfInterest = focusPoint
|
|
||||||
// device.exposureMode = .autoExpose
|
|
||||||
// }
|
|
||||||
device.unlockForConfiguration()
|
|
||||||
} catch {
|
|
||||||
self.error("Could not lock device for configuration: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension CameraView: Logger { }
|
|
@ -1,56 +0,0 @@
|
|||||||
/*
|
|
||||||
See LICENSE.txt for this sample’s licensing information.
|
|
||||||
|
|
||||||
Abstract:
|
|
||||||
Photo capture delegate.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import AVFoundation
|
|
||||||
import Photos
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
protocol PhotoCaptureHandlerDelegate {
|
|
||||||
|
|
||||||
func didCapture(_ image: UIImage?)
|
|
||||||
}
|
|
||||||
|
|
||||||
class PhotoCaptureHandler: NSObject {
|
|
||||||
|
|
||||||
var delegate: PhotoCaptureHandlerDelegate?
|
|
||||||
|
|
||||||
var photoSettings: AVCapturePhotoSettings {
|
|
||||||
let photoSettings = AVCapturePhotoSettings()
|
|
||||||
photoSettings.flashMode = .off
|
|
||||||
return photoSettings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PhotoCaptureHandler: AVCapturePhotoCaptureDelegate {
|
|
||||||
/*
|
|
||||||
This extension includes all the delegate callbacks for AVCapturePhotoCaptureDelegate protocol
|
|
||||||
*/
|
|
||||||
|
|
||||||
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
|
|
||||||
|
|
||||||
guard error == nil else {
|
|
||||||
log("PhotoCaptureHandler: \(error!)")
|
|
||||||
delegate?.didCapture(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let cgImage = photo.cgImageRepresentation() else {
|
|
||||||
log("PhotoCaptureHandler: No image captured")
|
|
||||||
delegate?.didCapture(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let image = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.delegate?.didCapture(image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PhotoCaptureHandler: Logger {
|
|
||||||
|
|
||||||
}
|
|
339
Caps/ContentView.swift
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
import BottomSheet
|
||||||
|
|
||||||
|
struct ContentView: View {
|
||||||
|
|
||||||
|
private let bottomIconSize: CGFloat = 25
|
||||||
|
|
||||||
|
private let bottomIconPadding: CGFloat = 7
|
||||||
|
|
||||||
|
private let capturedImageSize: CGFloat = 80
|
||||||
|
|
||||||
|
private let plusIconSize: CGFloat = 20
|
||||||
|
|
||||||
|
private var scale: CGFloat {
|
||||||
|
UIScreen.main.scale
|
||||||
|
}
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
var database: Database
|
||||||
|
|
||||||
|
@State
|
||||||
|
var searchString = ""
|
||||||
|
|
||||||
|
@State
|
||||||
|
var sortType: SortCriteria = .id
|
||||||
|
|
||||||
|
@State
|
||||||
|
var sortAscending = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
var showSortPopover = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
var showCameraSheet = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
var showSettingsSheet = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
var showGridView = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
var showNewClassifierAlert = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
var isEnteringNewCapName = false
|
||||||
|
|
||||||
|
@State
|
||||||
|
private var capIdOfNextPhoto: Int?
|
||||||
|
|
||||||
|
var filteredCaps: [Cap] {
|
||||||
|
let text = searchString
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
.lowercased()
|
||||||
|
guard text != "" else {
|
||||||
|
return Array(database.caps.values)
|
||||||
|
}
|
||||||
|
let textParts = text.components(separatedBy: " ").filter { $0 != "" }
|
||||||
|
return database.caps.values.compactMap { cap -> Cap? in
|
||||||
|
// For each part of text, check if name contains it
|
||||||
|
for textItem in textParts {
|
||||||
|
if !cap.cleanName.contains(textItem) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var shownCaps: [Cap] {
|
||||||
|
let caps = filteredCaps
|
||||||
|
if sortAscending {
|
||||||
|
switch sortType {
|
||||||
|
case .id:
|
||||||
|
return caps.sorted { $0.id < $1.id }
|
||||||
|
case .count:
|
||||||
|
return caps.sorted {
|
||||||
|
$0.imageCount < $1.imageCount
|
||||||
|
}
|
||||||
|
case .name:
|
||||||
|
return caps.sorted {
|
||||||
|
$0.name < $1.name
|
||||||
|
}
|
||||||
|
case .match:
|
||||||
|
return caps.sorted {
|
||||||
|
match(for: $0.id) ?? 0 < match(for: $1.id) ?? 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch sortType {
|
||||||
|
case .id:
|
||||||
|
return caps.sorted { $0.id > $1.id }
|
||||||
|
case .count:
|
||||||
|
return caps.sorted {
|
||||||
|
$0.imageCount > $1.imageCount
|
||||||
|
}
|
||||||
|
case .name:
|
||||||
|
return caps.sorted {
|
||||||
|
$0.name > $1.name
|
||||||
|
}
|
||||||
|
case .match:
|
||||||
|
return caps.sorted {
|
||||||
|
match(for: $0.id) ?? 0 > match(for: $1.id) ?? 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func match(for cap: Int) -> Float? {
|
||||||
|
database.matches[cap]
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationView {
|
||||||
|
ZStack {
|
||||||
|
List(shownCaps) { cap in
|
||||||
|
CapRowView(cap: cap, match: match(for: cap.id))
|
||||||
|
.onTapGesture {
|
||||||
|
didTap(cap: cap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.refreshable {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
.navigationTitle("Caps")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button(action: showSettings) {
|
||||||
|
Image(systemSymbol: .gearshape)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
if let image = database.image {
|
||||||
|
HStack(alignment: .bottom, spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
if isEnteringNewCapName {
|
||||||
|
Text(String(format: "ID: %d", database.nextCapId))
|
||||||
|
.font(.headline)
|
||||||
|
.padding(.vertical, 3)
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
.background(.regularMaterial)
|
||||||
|
.cornerRadius(5)
|
||||||
|
.padding(5)
|
||||||
|
}
|
||||||
|
Button(action: didTapClassifiedImage) {
|
||||||
|
ZStack(alignment: Alignment.bottomTrailing) {
|
||||||
|
Image(uiImage: image.resize(to: CGSize(width: capturedImageSize, height: capturedImageSize)))
|
||||||
|
.frame(width: capturedImageSize, height: capturedImageSize)
|
||||||
|
.background(.gray)
|
||||||
|
.cornerRadius(capturedImageSize/2)
|
||||||
|
.padding(5)
|
||||||
|
.background(.regularMaterial)
|
||||||
|
.cornerRadius(capturedImageSize/2 + 5)
|
||||||
|
.padding(5)
|
||||||
|
if !isEnteringNewCapName {
|
||||||
|
Image(systemSymbol: .plus)
|
||||||
|
.frame(width: plusIconSize, height: plusIconSize)
|
||||||
|
.padding(6)
|
||||||
|
.background(.regularMaterial)
|
||||||
|
.cornerRadius(plusIconSize/2 + 6)
|
||||||
|
.padding(5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
if isEnteringNewCapName {
|
||||||
|
Button(action: removeCapturedImage) {
|
||||||
|
Image(systemSymbol: .xmark)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: bottomIconSize-6,
|
||||||
|
height: bottomIconSize-6)
|
||||||
|
.padding()
|
||||||
|
.padding(3)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button(action: filter) {
|
||||||
|
Image(systemSymbol: .line3HorizontalDecreaseCircle)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: bottomIconSize,
|
||||||
|
height: bottomIconSize)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isEnteringNewCapName {
|
||||||
|
CapNameEntryView(name: $searchString)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
} else {
|
||||||
|
SearchField(searchString: $searchString)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
}
|
||||||
|
if isEnteringNewCapName {
|
||||||
|
Button(action: saveNewCap) {
|
||||||
|
Image(systemSymbol: .squareAndArrowDown)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: bottomIconSize-3,
|
||||||
|
height: bottomIconSize)
|
||||||
|
.padding()
|
||||||
|
.padding(.horizontal, (bottomIconPadding-3)/2)
|
||||||
|
}
|
||||||
|
} else if database.image != nil {
|
||||||
|
Button(action: removeCapturedImage) {
|
||||||
|
Image(systemSymbol: .xmark)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: bottomIconSize-6,
|
||||||
|
height: bottomIconSize-6)
|
||||||
|
.padding(3)
|
||||||
|
.padding(bottomIconPadding)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Button(action: openCamera) {
|
||||||
|
Image(systemSymbol: .camera)
|
||||||
|
.resizable()
|
||||||
|
.frame(width: bottomIconSize+bottomIconPadding,
|
||||||
|
height: bottomIconSize)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(.regularMaterial)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
UIScrollView.appearance().keyboardDismissMode = .onDrag
|
||||||
|
}
|
||||||
|
.bottomSheet(isPresented: $showSortPopover, height: 280) {
|
||||||
|
SortSelectionView(
|
||||||
|
hasMatches: !database.matches.isEmpty,
|
||||||
|
isPresented: $showSortPopover,
|
||||||
|
sortType: $sortType,
|
||||||
|
sortAscending: $sortAscending,
|
||||||
|
showGridView: $showGridView)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showCameraSheet) {
|
||||||
|
CameraView(isPresented: $showCameraSheet,
|
||||||
|
image: $database.image,
|
||||||
|
capId: $capIdOfNextPhoto)
|
||||||
|
}
|
||||||
|
.bottomSheet(isPresented: $showSettingsSheet, height: 360) {
|
||||||
|
SettingsView(isPresented: $showSettingsSheet)
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $showGridView) {
|
||||||
|
GridView()
|
||||||
|
}.alert(isPresented: $showNewClassifierAlert) {
|
||||||
|
Alert(title: Text("New classifier available"),
|
||||||
|
message: Text("Classifier \(database.serverClassifierVersion) is available. You have version \(database.classifierVersion). Do you want to download it now?"),
|
||||||
|
primaryButton: .default(Text("Download"), action: downloadClassifier),
|
||||||
|
secondaryButton: .cancel())
|
||||||
|
}
|
||||||
|
.onChange(of: database.image) { newImage in
|
||||||
|
if newImage != nil {
|
||||||
|
sortType = .id
|
||||||
|
sortAscending = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}.onChange(of: database.matches) { newMatches in
|
||||||
|
if newMatches.isEmpty {
|
||||||
|
sortType = .id
|
||||||
|
sortAscending = false
|
||||||
|
} else {
|
||||||
|
sortType = .match
|
||||||
|
sortAscending = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func refresh() {
|
||||||
|
Task {
|
||||||
|
await database.downloadCaps()
|
||||||
|
let hasNewClassifier = await database.serverHasNewClassifier()
|
||||||
|
guard hasNewClassifier else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.showNewClassifierAlert = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func filter() {
|
||||||
|
showSortPopover.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openCamera() {
|
||||||
|
removeCapturedImage()
|
||||||
|
showCameraSheet.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showSettings() {
|
||||||
|
showSettingsSheet.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func downloadClassifier() {
|
||||||
|
Task {
|
||||||
|
await database.downloadClassifier()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func didTapClassifiedImage() {
|
||||||
|
isEnteringNewCapName = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func removeCapturedImage() {
|
||||||
|
database.image = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func didTap(cap: Cap) {
|
||||||
|
guard let image = database.image else {
|
||||||
|
capIdOfNextPhoto = cap.id
|
||||||
|
openCamera()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
database.save(image, for: cap.id)
|
||||||
|
database.image = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveNewCap() {
|
||||||
|
guard let image = database.image else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let name = searchString.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let newCap = database.save(newCap: name)
|
||||||
|
database.save(image, for: newCap.id)
|
||||||
|
removeCapturedImage()
|
||||||
|
isEnteringNewCapName = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContentView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ContentView()
|
||||||
|
.environmentObject(Database.mock)
|
||||||
|
}
|
||||||
|
}
|
@ -1,199 +1,156 @@
|
|||||||
//
|
|
||||||
// Cap.swift
|
|
||||||
// CapCollector
|
|
||||||
//
|
|
||||||
// Created by Christoph on 19.11.18.
|
|
||||||
// Copyright © 2018 CH. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
|
||||||
import CoreImage
|
|
||||||
|
|
||||||
import SQLite
|
|
||||||
|
|
||||||
struct Cap {
|
struct Cap {
|
||||||
|
|
||||||
// MARK: - Static constants
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
private static let mosaicColumns = 40
|
|
||||||
|
|
||||||
static let mosaicCellSize: CGFloat = 60
|
|
||||||
|
|
||||||
private static let mosaicRowHeight = mosaicCellSize * 0.866
|
|
||||||
|
|
||||||
private static let mosaicMargin = mosaicCellSize - mosaicRowHeight
|
|
||||||
|
|
||||||
// MARK: - Variables
|
|
||||||
|
|
||||||
/// The unique number of the cap
|
/// The unique number of the cap
|
||||||
let id: Int
|
let id: Int
|
||||||
|
|
||||||
/// The name of the cap
|
/// The name of the cap
|
||||||
let name: String
|
var name: String
|
||||||
|
|
||||||
/// The name of the cap without special characters
|
/// The name of the cap without special characters
|
||||||
let cleanName: String
|
var cleanName: String
|
||||||
|
|
||||||
/// The number of images existing for the cap
|
/// The number of images existing for the cap
|
||||||
let count: Int
|
var imageCount: Int
|
||||||
|
|
||||||
/// Indicate if the cap can be found by the recognition model
|
/// The index of the main image for the cap
|
||||||
let matched: Bool
|
var mainImage: Int
|
||||||
|
|
||||||
/// Indicate if the cap is present on the server
|
/// The version of the first classifier capable of recognizing the cap
|
||||||
let uploaded: Bool
|
var classifierVersion: Int?
|
||||||
|
|
||||||
// MARK: Init
|
var color: Color?
|
||||||
|
|
||||||
init(name: String, id: Int) {
|
/// The subpath to the main image on the server
|
||||||
self.id = id
|
var mainImagePath: String {
|
||||||
self.count = 1
|
String(format: "images/%04d/%04d-%02d.jpg", id, id, mainImage)
|
||||||
self.name = name
|
|
||||||
self.cleanName = ""
|
|
||||||
self.matched = false
|
|
||||||
self.uploaded = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(id: Int, name: String, count: Int) {
|
/**
|
||||||
|
Create a new cap.
|
||||||
|
- Parameter id: The unique id of the cap
|
||||||
|
- Parameter name: The name associated with the cap
|
||||||
|
*/
|
||||||
|
init(id: Int, name: String, classifier: Int? = nil) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.name = name
|
self.name = name
|
||||||
self.count = count
|
|
||||||
self.cleanName = name.clean
|
self.cleanName = name.clean
|
||||||
self.matched = false
|
self.imageCount = 1
|
||||||
self.uploaded = true
|
self.mainImage = 0
|
||||||
|
self.classifierVersion = classifier
|
||||||
}
|
}
|
||||||
|
|
||||||
func renamed(to name: String) -> Cap {
|
init(data: CapData) {
|
||||||
Cap(from: self, renamed: name)
|
self.id = data.id
|
||||||
|
self.name = data.name
|
||||||
|
self.cleanName = data.name.clean
|
||||||
|
self.imageCount = data.count
|
||||||
|
self.mainImage = data.mainImage
|
||||||
|
self.classifierVersion = data.classifierVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
init(from cap: Cap, renamed newName: String) {
|
mutating func update(with data: CapData) {
|
||||||
self.id = cap.id
|
self.name = data.name
|
||||||
self.count = cap.count
|
self.cleanName = data.name.clean
|
||||||
self.name = newName
|
self.imageCount = data.count
|
||||||
self.cleanName = newName.clean
|
self.mainImage = data.mainImage
|
||||||
self.matched = cap.matched
|
self.classifierVersion = data.classifierVersion
|
||||||
self.uploaded = cap.uploaded
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: SQLite
|
static func ==(lhs: Cap, rhs: CapData) -> Bool {
|
||||||
|
lhs.id == rhs.id &&
|
||||||
init(row: Row) {
|
lhs.name == rhs.name &&
|
||||||
self.id = row[Cap.columnId]
|
lhs.imageCount == rhs.count &&
|
||||||
self.name = row[Cap.columnName]
|
lhs.mainImage == rhs.mainImage &&
|
||||||
self.count = row[Cap.columnCount]
|
lhs.classifierVersion == rhs.classifierVersion
|
||||||
self.cleanName = name.clean
|
|
||||||
self.matched = row[Cap.columnMatched]
|
|
||||||
self.uploaded = row[Cap.columnUploaded]
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static let table = Table("data")
|
static func !=(lhs: Cap, rhs: CapData) -> Bool {
|
||||||
|
!(lhs == rhs)
|
||||||
|
}
|
||||||
|
|
||||||
static var createQuery: String {
|
func classifiable(by classifierVersion: Int?) -> Bool {
|
||||||
table.create(ifNotExists: true) { t in
|
guard let version = classifierVersion else {
|
||||||
t.column(columnId, primaryKey: true)
|
return false
|
||||||
t.column(columnName)
|
}
|
||||||
t.column(columnCount)
|
guard let own = self.classifierVersion else {
|
||||||
t.column(columnMatched)
|
return false
|
||||||
t.column(columnUploaded)
|
}
|
||||||
|
return version >= own
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static let columnId = Expression<Int>("id")
|
extension Cap {
|
||||||
|
|
||||||
static let columnName = Expression<String>("name")
|
struct Color: Codable, Equatable {
|
||||||
|
|
||||||
static let columnCount = Expression<Int>("count")
|
let r: Int
|
||||||
|
|
||||||
static let columnMatched = Expression<Bool>("matched")
|
let g: Int
|
||||||
|
|
||||||
static let columnUploaded = Expression<Bool>("uploaded")
|
let b: Int
|
||||||
|
|
||||||
var insertQuery: Insert {
|
|
||||||
return Cap.table.insert(
|
|
||||||
Cap.columnId <- id,
|
|
||||||
Cap.columnName <- name,
|
|
||||||
Cap.columnCount <- count,
|
|
||||||
Cap.columnMatched <- matched,
|
|
||||||
Cap.columnUploaded <- uploaded)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Display
|
|
||||||
|
|
||||||
func matchLabelText(match: Float?, appIsUnlocked: Bool) -> String {
|
|
||||||
if let match = match {
|
|
||||||
let percent = Int((match * 100).rounded())
|
|
||||||
return String(format: "%d %%", arguments: [percent])
|
|
||||||
}
|
|
||||||
|
|
||||||
guard matched else {
|
|
||||||
return "📵"
|
|
||||||
}
|
|
||||||
guard appIsUnlocked, !hasSufficientImages else {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return "⚠️"
|
|
||||||
}
|
|
||||||
|
|
||||||
func countLabelText(appIsUnlocked: Bool) -> String {
|
|
||||||
guard appIsUnlocked else {
|
|
||||||
return "\(id)"
|
|
||||||
}
|
|
||||||
guard count != 1 else {
|
|
||||||
return "\(id) (1 image)"
|
|
||||||
}
|
|
||||||
return "\(id) (\(count) images)"
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Images
|
|
||||||
|
|
||||||
var hasSufficientImages: Bool {
|
|
||||||
count >= Cap.sufficientImageCount
|
|
||||||
}
|
|
||||||
|
|
||||||
static func thumbnail(for image: UIImage) -> UIImage {
|
|
||||||
let len = GridViewController.len * 2
|
|
||||||
return image.resize(to: CGSize.init(width: len, height: len))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Protocol Hashable
|
// MARK: Protocol Identifiable
|
||||||
|
|
||||||
extension Cap: Hashable {
|
extension Cap: Identifiable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Protocol Comparable
|
||||||
|
|
||||||
|
extension Cap: Codable {
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id = "u"
|
||||||
|
case name = "n"
|
||||||
|
case cleanName = "c"
|
||||||
|
case imageCount = "i"
|
||||||
|
case mainImage = "m"
|
||||||
|
case classifierVersion = "v"
|
||||||
|
case color = "f"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Protocol Comparable
|
||||||
|
|
||||||
|
extension Cap: Comparable {
|
||||||
|
|
||||||
|
static func < (lhs: Cap, rhs: Cap) -> Bool {
|
||||||
|
lhs.id < rhs.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Protocol Equatable
|
||||||
|
|
||||||
|
extension Cap: Equatable {
|
||||||
|
|
||||||
static func == (lhs: Cap, rhs: Cap) -> Bool {
|
static func == (lhs: Cap, rhs: Cap) -> Bool {
|
||||||
return lhs.id == rhs.id
|
return lhs.id == rhs.id
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Protocol Hashable
|
||||||
|
|
||||||
|
extension Cap: Hashable {
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Protocol Logger
|
// MARK: String extension
|
||||||
|
|
||||||
extension Cap: Logger { }
|
private extension String {
|
||||||
|
|
||||||
// MARK: - String extension
|
|
||||||
|
|
||||||
extension String {
|
|
||||||
|
|
||||||
var clean: String {
|
var clean: String {
|
||||||
return lowercased().replacingOccurrences(of: "[^a-z0-9 ]", with: "", options: .regularExpression)
|
return lowercased().replacingOccurrences(of: "[^a-z0-9 ]", with: "", options: .regularExpression)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Int extension
|
// MARK: Int extension
|
||||||
|
|
||||||
private extension Int {
|
private extension Int {
|
||||||
|
|
||||||
|
32
Caps/Data/CapData.swift
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct CapData: Codable {
|
||||||
|
|
||||||
|
let id: Int
|
||||||
|
|
||||||
|
var name: String
|
||||||
|
|
||||||
|
var count: Int
|
||||||
|
|
||||||
|
var mainImage: Int
|
||||||
|
|
||||||
|
var classifierVersion: Int?
|
||||||
|
|
||||||
|
var color: Cap.Color?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case id = "i"
|
||||||
|
case name = "n"
|
||||||
|
case count = "c"
|
||||||
|
case mainImage = "m"
|
||||||
|
case classifierVersion = "v"
|
||||||
|
case color = "f"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CapData: Comparable {
|
||||||
|
|
||||||
|
static func < (lhs: CapData, rhs: CapData) -> Bool {
|
||||||
|
lhs.id < rhs.id
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,3 @@
|
|||||||
//
|
|
||||||
// VisionHandler.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 12.02.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Vision
|
import Vision
|
||||||
import CoreML
|
import CoreML
|
||||||
@ -24,17 +16,13 @@ class Classifier: Logger {
|
|||||||
/**
|
/**
|
||||||
Classify an image
|
Classify an image
|
||||||
- Parameter image: The image to classify
|
- Parameter image: The image to classify
|
||||||
|
- Parameter completion: The callback with the match results
|
||||||
|
- Parameter matches: A dictionary with a map from cap id to classifier match
|
||||||
- Note: This method should not be scheduled on the main thread.
|
- Note: This method should not be scheduled on the main thread.
|
||||||
*/
|
*/
|
||||||
func recognize(image: UIImage, completion: @escaping (_ matches: [Int: Float]?) -> Void) {
|
func recognize(image: CGImage, completion: @escaping (_ matches: [Int: Float]?) -> Void) {
|
||||||
guard let ciImage = CIImage(image: image) else {
|
let image = CIImage(cgImage: image)
|
||||||
error("Unable to create CIImage")
|
let handler = VNImageRequestHandler(ciImage: image, orientation: .up)
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let orientation = CGImagePropertyOrientation(image.imageOrientation)
|
|
||||||
let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
|
|
||||||
let request = VNCoreMLRequest(model: model) { request, error in
|
let request = VNCoreMLRequest(model: model) { request, error in
|
||||||
let matches = self.process(request: request, error: error)
|
let matches = self.process(request: request, error: error)
|
||||||
completion(matches)
|
completion(matches)
|
||||||
|
@ -1,132 +0,0 @@
|
|||||||
//
|
|
||||||
// Colors.swift
|
|
||||||
// CapCollector
|
|
||||||
//
|
|
||||||
// Created by Christoph on 26.05.20.
|
|
||||||
// Copyright © 2020 CH. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import SQLite
|
|
||||||
|
|
||||||
extension Database {
|
|
||||||
|
|
||||||
enum Colors {
|
|
||||||
|
|
||||||
static let table = Table("colors")
|
|
||||||
|
|
||||||
static let columnRed = Expression<Double>("red")
|
|
||||||
|
|
||||||
static let columnGreen = Expression<Double>("green")
|
|
||||||
|
|
||||||
static let columnBlue = Expression<Double>("blue")
|
|
||||||
|
|
||||||
static var createQuery: String {
|
|
||||||
table.create(ifNotExists: true) { t in
|
|
||||||
t.column(Cap.columnId, primaryKey: true)
|
|
||||||
t.column(columnRed)
|
|
||||||
t.column(columnGreen)
|
|
||||||
t.column(columnBlue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var colors: [Int : UIColor] {
|
|
||||||
do {
|
|
||||||
let rows = try db.prepare(Database.Colors.table)
|
|
||||||
return rows.reduce(into: [:]) { dict, row in
|
|
||||||
let id = row[Cap.columnId]
|
|
||||||
let r = CGFloat(row[Database.Colors.columnRed])
|
|
||||||
let g = CGFloat(row[Database.Colors.columnGreen])
|
|
||||||
let b = CGFloat(row[Database.Colors.columnBlue])
|
|
||||||
dict[id] = UIColor(red: r, green: g, blue: b, alpha: 1.0)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
log("Failed to load cap colors: \(error)")
|
|
||||||
return [:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var capsWithColors: Set<Int> {
|
|
||||||
do {
|
|
||||||
let rows = try db.prepare(Database.Colors.table.select(Cap.columnId))
|
|
||||||
return Set(rows.map { $0[Cap.columnId]})
|
|
||||||
} catch {
|
|
||||||
log("Failed to load caps with colors: \(error)")
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var capsWithoutColors: Set<Int> {
|
|
||||||
Set(1...capCount).subtracting(capsWithColors)
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeColor(for cap: Int) -> Bool {
|
|
||||||
do {
|
|
||||||
try db.run(Colors.table.filter(Cap.columnId == cap).delete())
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
log("Failed to delete cap color \(cap): \(error)")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func set(color: UIColor, for cap: Int) -> Bool {
|
|
||||||
guard let _ = row(for: cap) else {
|
|
||||||
return insert(color: color, for: cap)
|
|
||||||
}
|
|
||||||
return update(color: color, for: cap)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func insert(color: UIColor, for cap: Int) -> Bool {
|
|
||||||
let (red, green, blue) = color.rgb
|
|
||||||
let query = Database.Colors.table.insert(
|
|
||||||
Cap.columnId <- cap,
|
|
||||||
Database.Colors.columnRed <- red,
|
|
||||||
Database.Colors.columnGreen <- green,
|
|
||||||
Database.Colors.columnBlue <- blue)
|
|
||||||
|
|
||||||
do {
|
|
||||||
try db.run(query)
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
log("Failed to insert color for cap \(cap): \(error)")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func update(color: UIColor, for cap: Int) -> Bool {
|
|
||||||
let (red, green, blue) = color.rgb
|
|
||||||
let query = Database.Colors.table.filter(Cap.columnId == cap).update(
|
|
||||||
Database.Colors.columnRed <- red,
|
|
||||||
Database.Colors.columnGreen <- green,
|
|
||||||
Database.Colors.columnBlue <- blue)
|
|
||||||
|
|
||||||
do {
|
|
||||||
try db.run(query)
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
log("Failed to update color for cap \(cap): \(error)")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func row(for cap: Int) -> Row? {
|
|
||||||
do {
|
|
||||||
return try db.pluck(Database.Colors.table.filter(Cap.columnId == cap))
|
|
||||||
} catch {
|
|
||||||
log("Failed to get color for cap \(cap): \(error)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func color(for cap: Int) -> UIColor? {
|
|
||||||
guard let row = self.row(for: cap) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let r = CGFloat(row[Database.Colors.columnRed])
|
|
||||||
let g = CGFloat(row[Database.Colors.columnGreen])
|
|
||||||
let b = CGFloat(row[Database.Colors.columnBlue])
|
|
||||||
return UIColor(red: r, green: g, blue: b, alpha: 1.0)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,358 +0,0 @@
|
|||||||
//
|
|
||||||
// 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: .ephemeral, delegate: delegate, delegateQueue: nil)
|
|
||||||
self.delegate = delegate
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Paths
|
|
||||||
|
|
||||||
var serverNameListUrl: URL {
|
|
||||||
Download.serverNameListUrl(server: serverUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func serverNameListUrl(server: URL) -> URL {
|
|
||||||
server.appendingPathComponent("names.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
private var serverClassifierVersionUrl: URL {
|
|
||||||
serverUrl.appendingPathComponent("classifier.version")
|
|
||||||
}
|
|
||||||
|
|
||||||
private var serverRecognitionModelUrl: URL {
|
|
||||||
serverUrl.appendingPathComponent("classifier.mlmodel")
|
|
||||||
}
|
|
||||||
|
|
||||||
private var serverAllCountsUrl: URL {
|
|
||||||
serverUrl.appendingPathComponent("counts")
|
|
||||||
}
|
|
||||||
|
|
||||||
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 serverNameUrl(for cap: Int) -> URL {
|
|
||||||
serverUrl.appendingPathComponent("name/\(cap)")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func serverImageCountUrl(for cap: Int) -> URL {
|
|
||||||
serverUrl.appendingPathComponent("count/\(cap)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
func image(for cap: Int, to url: URL, timeout: TimeInterval = 30) -> Bool {
|
|
||||||
let group = DispatchGroup()
|
|
||||||
group.enter()
|
|
||||||
var result = true
|
|
||||||
let success = image(for: cap, version: 0, to: url) { success in
|
|
||||||
result = success
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
guard success else {
|
|
||||||
log("Already downloading image for cap \(cap)")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
guard group.wait(timeout: .now() + timeout) == .success else {
|
|
||||||
log("Timed out downloading image for cap \(cap)")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
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
|
|
||||||
- Returns: `true`, of the file download was started, `false`, if the image is already downloading.
|
|
||||||
*/
|
|
||||||
@discardableResult
|
|
||||||
func image(for cap: Int, version: Int = 0, to url: URL, queue: DispatchQueue = .main, completion: @escaping (Bool) -> Void) -> Bool {
|
|
||||||
// Check if main image, and already being downloaded
|
|
||||||
if version == 0 {
|
|
||||||
guard !downloadingMainImages.contains(cap) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
downloadingMainImages.insert(cap)
|
|
||||||
}
|
|
||||||
let serverUrl = serverImageUrl(for: cap, version: version)
|
|
||||||
let query = "Image of cap \(cap) version \(version)"
|
|
||||||
let task = session.downloadTask(with: serverUrl) { fileUrl, response, error in
|
|
||||||
if version == 0 {
|
|
||||||
queue.async {
|
|
||||||
self.downloadingMainImages.remove(cap)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
guard let fileUrl = self.convertResponse(to: query, fileUrl, response, error) else {
|
|
||||||
completion(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
if FileManager.default.fileExists(atPath: url.path) {
|
|
||||||
try FileManager.default.removeItem(at: url)
|
|
||||||
}
|
|
||||||
try FileManager.default.moveItem(at: fileUrl, to: url)
|
|
||||||
} catch {
|
|
||||||
self.log("Failed to move downloaded image for cap \(cap): \(error)")
|
|
||||||
completion(false)
|
|
||||||
}
|
|
||||||
completion(true)
|
|
||||||
}
|
|
||||||
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)"
|
|
||||||
session.startTaskExpectingInt(with: url, query: query, completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func name(for cap: Int, completion: @escaping (_ name: String?) -> Void) {
|
|
||||||
let url = serverNameUrl(for: cap)
|
|
||||||
let query = "Name for cap \(cap)"
|
|
||||||
session.startTaskExpectingString(with: url, query: query, completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func imageCounts(completion: @escaping ([Int]?) -> Void) {
|
|
||||||
let query = "Image count of all caps"
|
|
||||||
session.startTaskExpectingData(with: serverAllCountsUrl, query: query) { data in
|
|
||||||
guard let data = data else {
|
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
completion(data.map(Int.init))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func names(completion: @escaping ([String]?) -> Void) {
|
|
||||||
let query = "Download of server database"
|
|
||||||
session.startTaskExpectingString(with: serverNameListUrl, query: query) { string in
|
|
||||||
completion(string?.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: "\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func databaseSize(completion: @escaping (_ size: Int64?) -> Void) {
|
|
||||||
size(of: "database size", to: serverNameListUrl, completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func classifierVersion(completion: @escaping (Int?) -> Void) {
|
|
||||||
let query = "Server classifier version"
|
|
||||||
session.startTaskExpectingInt(with: serverClassifierVersionUrl, query: query, completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 { }
|
|
||||||
|
|
||||||
extension URLSession {
|
|
||||||
|
|
||||||
func startTaskExpectingData(with url: URL, query: String, completion: @escaping (Data?) -> Void) {
|
|
||||||
let task = dataTask(with: url) { data, response, error in
|
|
||||||
if let error = error {
|
|
||||||
log("Request '\(query)' produced an error: \(error)")
|
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let response = response else {
|
|
||||||
log("Request '\(query)' received no response")
|
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let urlResponse = response as? HTTPURLResponse else {
|
|
||||||
log("Request '\(query)' received an invalid response: \(response)")
|
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard urlResponse.statusCode == 200 else {
|
|
||||||
log("Request '\(query)' failed with status code \(urlResponse.statusCode)")
|
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let d = data else {
|
|
||||||
log("Request '\(query)' received no data")
|
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
completion(d)
|
|
||||||
}
|
|
||||||
task.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
func startTaskExpectingString(with url: URL, query: String, completion: @escaping (String?) -> Void) {
|
|
||||||
startTaskExpectingData(with: url, query: query) { data in
|
|
||||||
guard let data = data else {
|
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let string = String(data: data, encoding: .utf8) else {
|
|
||||||
log("Request '\(query)' received invalid data (not a string)")
|
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
completion(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func startTaskExpectingInt(with url: URL, query: String, completion: @escaping (Int?) -> Void) {
|
|
||||||
startTaskExpectingString(with: url, query: query) { string in
|
|
||||||
guard let string = string else {
|
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let int = Int(string.trimmingCharacters(in: .whitespacesAndNewlines)) else {
|
|
||||||
log("Request '\(query)' received an invalid value '\(string)'")
|
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
completion(int)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
28
Caps/Data/SortCriteria.swift
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum SortCriteria: Int, CaseIterable {
|
||||||
|
case id = 0
|
||||||
|
case name = 1
|
||||||
|
case count = 2
|
||||||
|
case match = 3
|
||||||
|
|
||||||
|
var text: String {
|
||||||
|
switch self {
|
||||||
|
case .id:
|
||||||
|
return "Id"
|
||||||
|
case .name:
|
||||||
|
return "Name"
|
||||||
|
case .count:
|
||||||
|
return "Count"
|
||||||
|
case .match:
|
||||||
|
return "Match"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SortCriteria: Identifiable {
|
||||||
|
|
||||||
|
var id: Int {
|
||||||
|
rawValue
|
||||||
|
}
|
||||||
|
}
|
@ -1,374 +0,0 @@
|
|||||||
//
|
|
||||||
// DiskManager.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 23.04.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import CoreML
|
|
||||||
import Vision
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
protocol ImageProvider: AnyObject {
|
|
||||||
|
|
||||||
func image(for cap: Int) -> UIImage?
|
|
||||||
|
|
||||||
func image(for cap: Int, version: Int) -> UIImage?
|
|
||||||
|
|
||||||
func ciImage(for cap: Int) -> CIImage?
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol ThumbnailCreationDelegate {
|
|
||||||
|
|
||||||
func thumbnailCreation(progress: Int, total: Int)
|
|
||||||
|
|
||||||
func thumbnailCreationFailed()
|
|
||||||
|
|
||||||
func thumbnailCreationIsMissingImages()
|
|
||||||
|
|
||||||
func thumbnailCreationCompleted()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
final class Storage: ImageProvider {
|
|
||||||
|
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
|
|
||||||
func localImageUrl(for cap: Int, version: Int = 0) -> URL {
|
|
||||||
baseUrl.appendingPathComponent("\(cap)-\(version).jpg")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func thumbnailUrl(for cap: Int) -> URL {
|
|
||||||
baseUrl.appendingPathComponent("\(cap)-thumb.jpg")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func tileImageUrl(for image: String) -> URL {
|
|
||||||
baseUrl.appendingPathComponent(image.clean + ".tile")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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) -> UIImage? {
|
|
||||||
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 UIImage(contentsOfFile: targetUrl.path)
|
|
||||||
} catch {
|
|
||||||
log("Failed to delete or move image \(version) for cap \(cap)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
private 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: High-level functions
|
|
||||||
|
|
||||||
func switchMainImage(to version: Int, for cap: Int) -> Bool {
|
|
||||||
guard deleteThumbnail(for: cap) else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
let newImagePath = localImageUrl(for: cap, version: version)
|
|
||||||
guard fm.fileExists(atPath: newImagePath.path) else {
|
|
||||||
return deleteImage(for: cap, version: version)
|
|
||||||
}
|
|
||||||
let oldImagePath = localImageUrl(for: cap, version: 0)
|
|
||||||
return move(newImagePath, to: oldImagePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Check if a thumbnail exists for a cap
|
|
||||||
- parameter cap: The id of the cap
|
|
||||||
- returns: True, if a thumbnail exists
|
|
||||||
*/
|
|
||||||
func hasThumbnail(for cap: Int) -> Bool {
|
|
||||||
fm.fileExists(atPath: thumbnailUrl(for: cap).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) -> 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
|
|
||||||
}
|
|
||||||
|
|
||||||
func image(for cap: Int) -> UIImage? {
|
|
||||||
image(for: cap, version: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
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 ciImage(for cap: Int) -> CIImage? {
|
|
||||||
let url = thumbnailUrl(for: cap)
|
|
||||||
guard fm.fileExists(atPath: url.path) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let image = CIImage(contentsOf: url) else {
|
|
||||||
error("Failed to read CIImage for main image of cap \(cap)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return image
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
let url = localImageUrl(for: cap, version: version)
|
|
||||||
return delete(at: url)
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func deleteThumbnail(for cap: Int) -> Bool {
|
|
||||||
let url = thumbnailUrl(for: cap)
|
|
||||||
return delete(at: url)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func delete(at url: URL) -> Bool {
|
|
||||||
guard fm.fileExists(atPath: url.path) else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
try fm.removeItem(at: url)
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
log("Failed to delete file \(url.lastPathComponent): \(error)")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Storage: Logger { }
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
|||||||
//
|
|
||||||
// TileImage.swift
|
|
||||||
// CapCollector
|
|
||||||
//
|
|
||||||
// Created by Christoph on 20.05.20.
|
|
||||||
// Copyright © 2020 CH. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import SQLite
|
|
||||||
|
|
||||||
extension Database {
|
|
||||||
|
|
||||||
struct TileImage {
|
|
||||||
|
|
||||||
let name: String
|
|
||||||
|
|
||||||
let width: Int
|
|
||||||
|
|
||||||
/// The tiles of each cap, with the index being the tile, and the value being the cap id.
|
|
||||||
let caps: [Int]
|
|
||||||
|
|
||||||
var encodedCaps: Data {
|
|
||||||
caps.map(UInt16.init).withUnsafeBytes { (p) in
|
|
||||||
Data(buffer: p.bindMemory(to: UInt8.self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(name: String, width: Int, caps: [Int]) {
|
|
||||||
self.name = name
|
|
||||||
self.width = width
|
|
||||||
self.caps = caps
|
|
||||||
}
|
|
||||||
|
|
||||||
init(row: Row) {
|
|
||||||
self.name = row[TileImage.columnName]
|
|
||||||
self.width = row[TileImage.columnWidth]
|
|
||||||
self.caps = row[TileImage.columnCaps].withUnsafeBytes { p in
|
|
||||||
p.bindMemory(to: UInt16.self).map(Int.init)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var insertQuery: Insert {
|
|
||||||
TileImage.table.insert(
|
|
||||||
TileImage.columnName <- name,
|
|
||||||
TileImage.columnWidth <- width,
|
|
||||||
TileImage.columnCaps <- encodedCaps)
|
|
||||||
}
|
|
||||||
|
|
||||||
var updateQuery: Update {
|
|
||||||
TileImage.table.update(
|
|
||||||
TileImage.columnWidth <- width,
|
|
||||||
TileImage.columnCaps <- encodedCaps)
|
|
||||||
}
|
|
||||||
|
|
||||||
static let columnName = Expression<String>("name")
|
|
||||||
|
|
||||||
static let columnWidth = Expression<Int>("width")
|
|
||||||
|
|
||||||
static let columnCaps = Expression<Data>("caps")
|
|
||||||
|
|
||||||
static let table = Table("images")
|
|
||||||
|
|
||||||
|
|
||||||
static func named(_ name: String) -> Table {
|
|
||||||
table.filter(columnName == name)
|
|
||||||
}
|
|
||||||
static func exists(_ name: String) -> Table {
|
|
||||||
named(name).select(columnName)
|
|
||||||
}
|
|
||||||
|
|
||||||
static var createQuery: String {
|
|
||||||
table.create(ifNotExists: true) { t in
|
|
||||||
t.column(Cap.columnId, primaryKey: true)
|
|
||||||
t.column(columnName)
|
|
||||||
t.column(columnWidth)
|
|
||||||
t.column(columnCaps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func save(tileImage: TileImage) -> Bool {
|
|
||||||
guard exists(tileImage.name) else {
|
|
||||||
return insert(tileImage)
|
|
||||||
}
|
|
||||||
return update(tileImage)
|
|
||||||
}
|
|
||||||
|
|
||||||
var tileImages: [TileImage] {
|
|
||||||
(try? db.prepare(TileImage.table).map(TileImage.init)) ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
func tileImage(named name: String) -> TileImage? {
|
|
||||||
do {
|
|
||||||
guard let row = try db.pluck(TileImage.named(name)) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return TileImage(row: row)
|
|
||||||
} catch {
|
|
||||||
log("Failed to get tile image \(name): \(error)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func exists(_ tileImage: String) -> Bool {
|
|
||||||
do {
|
|
||||||
return try db.pluck(TileImage.exists(tileImage)) != nil
|
|
||||||
} catch {
|
|
||||||
log("Failed to check tile image \(tileImage): \(error)")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func insert(_ tileImage: TileImage) -> Bool {
|
|
||||||
do {
|
|
||||||
try db.run(tileImage.insertQuery)
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
log("Failed to insert tile image \(tileImage): \(error)")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func update(_ tileImage: TileImage) -> Bool {
|
|
||||||
do {
|
|
||||||
try db.run(tileImage.updateQuery)
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
log("Failed to update tile image \(tileImage): \(error)")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,212 +0,0 @@
|
|||||||
//
|
|
||||||
// Upload.swift
|
|
||||||
// CapCollector
|
|
||||||
//
|
|
||||||
// Created by Christoph on 26.04.20.
|
|
||||||
// Copyright © 2020 CH. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import SQLite
|
|
||||||
|
|
||||||
struct Upload {
|
|
||||||
|
|
||||||
static let offlineKey = "offline"
|
|
||||||
|
|
||||||
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 existsQuery(for cap: Int, version: Int) -> ScalarQuery<Int> {
|
|
||||||
table.filter(rowCapId == cap && rowCapVersion == version).count
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
completion(true)
|
|
||||||
}
|
|
||||||
task.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
func upload(_ cap: Cap, timeout: TimeInterval = 30) -> Bool {
|
|
||||||
upload(name: cap.name, for: cap.id, timeout: timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func upload(name: String, for cap: Int, timeout: TimeInterval = 30) -> Bool {
|
|
||||||
let group = DispatchGroup()
|
|
||||||
group.enter()
|
|
||||||
var result = true
|
|
||||||
upload(name: name, for: cap) { success in
|
|
||||||
if success {
|
|
||||||
self.log("Uploaded cap \(cap)")
|
|
||||||
} else {
|
|
||||||
result = false
|
|
||||||
}
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
guard group.wait(timeout: .now() + timeout) == .success else {
|
|
||||||
log("Timed out uploading cap \(cap)")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func upload(imageAt url: URL, for cap: Int, completion: @escaping (_ count: Int?) -> Void) {
|
|
||||||
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 of cap \(cap): \(error)")
|
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let response = response else {
|
|
||||||
self.log("Failed to upload image of cap \(cap): No response")
|
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let urlResponse = response as? HTTPURLResponse else {
|
|
||||||
self.log("Failed to upload image of cap \(cap): \(response)")
|
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard urlResponse.statusCode == 200 else {
|
|
||||||
self.log("Failed to upload image 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 of cap \(cap): Invalid response")
|
|
||||||
completion(nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
completion(int)
|
|
||||||
}
|
|
||||||
task.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
func upload(imageAt url: URL, of cap: Int, timeout: TimeInterval = 30) -> Int? {
|
|
||||||
let group = DispatchGroup()
|
|
||||||
group.enter()
|
|
||||||
var result: Int? = nil
|
|
||||||
upload(imageAt: url, for: cap) { count in
|
|
||||||
result = count
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
guard group.wait(timeout: .now() + timeout) == .success else {
|
|
||||||
log("Timed out uploading image of \(cap)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
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 result on completion.
|
|
||||||
*/
|
|
||||||
func setMainImage(for cap: Int, to version: Int, completion: @escaping (_ success: Bool) -> Void) {
|
|
||||||
let url = serverChangeMainImageUrl(for: cap, to: version)
|
|
||||||
let task = URLSession.shared.dataTask(with: url) { data, response, error in
|
|
||||||
if let error = error {
|
|
||||||
self.log("Failed to set main image of cap \(cap) to \(version): \(error)")
|
|
||||||
completion(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let response = response else {
|
|
||||||
self.log("Failed to set main image of cap \(cap) to \(version): No response")
|
|
||||||
completion(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let urlResponse = response as? HTTPURLResponse else {
|
|
||||||
self.log("Failed to set main image of cap \(cap) to \(version): \(response)")
|
|
||||||
completion(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard urlResponse.statusCode == 200 else {
|
|
||||||
self.log("Failed to set main image of cap \(cap) to \(version): Response \(urlResponse.statusCode)")
|
|
||||||
completion(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
completion(true)
|
|
||||||
}
|
|
||||||
task.resume()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Upload: Logger { }
|
|
@ -1,30 +0,0 @@
|
|||||||
//
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
30
Caps/Extensions/CGImage+Extensions.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import CoreGraphics
|
||||||
|
import VideoToolbox
|
||||||
|
|
||||||
|
extension CGImage {
|
||||||
|
|
||||||
|
static func create(from cvPixelBuffer: CVPixelBuffer?) -> CGImage? {
|
||||||
|
guard let pixelBuffer = cvPixelBuffer else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var image: CGImage?
|
||||||
|
VTCreateCGImageFromCVPixelBuffer(pixelBuffer, options: nil, imageOut: &image)
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Crop an image to a square, centered around the middle of the frame
|
||||||
|
- parameter size: The height and width of the resulting image
|
||||||
|
- returns: The cropped image
|
||||||
|
*/
|
||||||
|
func crop(to size: CGFloat) -> CGImage? {
|
||||||
|
let rect = CGRect(
|
||||||
|
x: (CGFloat(height) - size) / 2,
|
||||||
|
y: (CGFloat(width) - size) / 2,
|
||||||
|
width: size,
|
||||||
|
height: size)
|
||||||
|
|
||||||
|
return cropping(to: rect)
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,3 @@
|
|||||||
//
|
|
||||||
// CGImagePropertyOrientation+Extensions.swift
|
|
||||||
// CapCollector
|
|
||||||
//
|
|
||||||
// Created by Christoph on 13.05.20.
|
|
||||||
// Copyright © 2020 CH. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
extension CGImagePropertyOrientation {
|
extension CGImagePropertyOrientation {
|
||||||
|
59
Caps/Extensions/Data+Extensions.swift
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Data {
|
||||||
|
|
||||||
|
public var hexEncoded: String {
|
||||||
|
return map { String(format: "%02hhx", $0) }.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert 0 ... 9, a ... f, A ...F to their decimal value,
|
||||||
|
// return nil for all other input characters
|
||||||
|
private func decodeNibble(_ u: UInt16) -> UInt8? {
|
||||||
|
switch(u) {
|
||||||
|
case 0x30 ... 0x39:
|
||||||
|
return UInt8(u - 0x30)
|
||||||
|
case 0x41 ... 0x46:
|
||||||
|
return UInt8(u - 0x41 + 10)
|
||||||
|
case 0x61 ... 0x66:
|
||||||
|
return UInt8(u - 0x61 + 10)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init?(fromHexEncodedString string: String) {
|
||||||
|
let utf16 = string.utf16
|
||||||
|
self.init(capacity: utf16.count/2)
|
||||||
|
|
||||||
|
var i = utf16.startIndex
|
||||||
|
guard utf16.count % 2 == 0 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
while i != utf16.endIndex {
|
||||||
|
guard let hi = decodeNibble(utf16[i]),
|
||||||
|
let lo = decodeNibble(utf16[utf16.index(i, offsetBy: 1, limitedBy: utf16.endIndex)!]) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var value = hi << 4 + lo
|
||||||
|
self.append(&value, count: 1)
|
||||||
|
i = utf16.index(i, offsetBy: 2, limitedBy: utf16.endIndex)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Data {
|
||||||
|
|
||||||
|
|
||||||
|
func convert<T>(into value: T) -> T {
|
||||||
|
withUnsafeBytes {
|
||||||
|
$0.baseAddress!.load(as: T.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init<T>(from value: T) {
|
||||||
|
var target = value
|
||||||
|
self = Swift.withUnsafeBytes(of: &target) {
|
||||||
|
Data($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,28 +0,0 @@
|
|||||||
//
|
|
||||||
// DispatchGroup+Extensions.swift
|
|
||||||
// CapCollector
|
|
||||||
//
|
|
||||||
// Created by iMac on 13.01.21.
|
|
||||||
// Copyright © 2021 CH. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
extension DispatchGroup {
|
|
||||||
|
|
||||||
typealias AsyncSuccessCallback = (Bool) -> Void
|
|
||||||
|
|
||||||
static func singleTask(timeout: TimeInterval = 30, _ block: (@escaping AsyncSuccessCallback) -> Void) -> Bool {
|
|
||||||
let group = DispatchGroup()
|
|
||||||
group.enter()
|
|
||||||
var result = true
|
|
||||||
block { success in
|
|
||||||
result = success
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
guard group.wait(timeout: .now() + timeout) == .success else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
//
|
|
||||||
// UIAlertControllerExtensions.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 23.03.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
extension UIAlertController {
|
|
||||||
|
|
||||||
private struct AssociatedKeys {
|
|
||||||
static var blurStyleKey = "UIAlertController.blurStyleKey"
|
|
||||||
}
|
|
||||||
|
|
||||||
public var blurStyle: UIBlurEffect.Style {
|
|
||||||
get {
|
|
||||||
return objc_getAssociatedObject(self, &AssociatedKeys.blurStyleKey) as? UIBlurEffect.Style ?? .extraLight
|
|
||||||
} set (style) {
|
|
||||||
objc_setAssociatedObject(self, &AssociatedKeys.blurStyleKey, style, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
||||||
|
|
||||||
view.setNeedsLayout()
|
|
||||||
view.layoutIfNeeded()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var cancelButtonColor: UIColor? {
|
|
||||||
return blurStyle == .dark ? UIColor(red: 28.0/255.0, green: 28.0/255.0, blue: 28.0/255.0, alpha: 1.0) : nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private var visualEffectView: UIVisualEffectView? {
|
|
||||||
if let presentationController = presentationController, presentationController.responds(to: Selector(("popoverView"))), let view = presentationController.value(forKey: "popoverView") as? UIView // We're on an iPad and visual effect view is in a different place.
|
|
||||||
{
|
|
||||||
return view.recursiveSubviews.compactMap({$0 as? UIVisualEffectView}).first
|
|
||||||
}
|
|
||||||
|
|
||||||
return view.recursiveSubviews.compactMap({$0 as? UIVisualEffectView}).first
|
|
||||||
}
|
|
||||||
|
|
||||||
private var cancelActionView: UIView? {
|
|
||||||
return view.recursiveSubviews.compactMap({
|
|
||||||
$0 as? UILabel}
|
|
||||||
).first(where: {
|
|
||||||
$0.text == actions.first(where: { $0.style == .cancel })?.title
|
|
||||||
})?.superview?.superview
|
|
||||||
}
|
|
||||||
|
|
||||||
public convenience init(title: String?, message: String?, preferredStyle: UIAlertController.Style, blurStyle: UIBlurEffect.Style) {
|
|
||||||
self.init(title: title, message: message, preferredStyle: preferredStyle)
|
|
||||||
self.blurStyle = blurStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
open override func viewWillLayoutSubviews() {
|
|
||||||
super.viewWillLayoutSubviews()
|
|
||||||
|
|
||||||
visualEffectView?.effect = UIBlurEffect(style: blurStyle)
|
|
||||||
cancelActionView?.backgroundColor = cancelButtonColor
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
//
|
|
||||||
// UIColor+Extensions.swift
|
|
||||||
// CapCollector
|
|
||||||
//
|
|
||||||
// Created by Christoph on 14.04.20.
|
|
||||||
// Copyright © 2020 CH. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
extension UIColor {
|
|
||||||
|
|
||||||
var rgb: (red: Double, green: Double, blue: Double) {
|
|
||||||
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 (Double(fRed), Double(fGreen), Double(fBlue))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +1,3 @@
|
|||||||
//
|
|
||||||
// UIImageExtensions.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 13.02.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
@ -22,7 +14,7 @@ extension UIImage {
|
|||||||
func resize(to targetSize: CGSize) -> UIImage {
|
func resize(to targetSize: CGSize) -> UIImage {
|
||||||
let rect = CGRect(x: 0, y: 0, width: targetSize.width, height: targetSize.height)
|
let rect = CGRect(x: 0, y: 0, width: targetSize.width, height: targetSize.height)
|
||||||
|
|
||||||
UIGraphicsBeginImageContextWithOptions(targetSize, false, 1.0)
|
UIGraphicsBeginImageContextWithOptions(targetSize, false, scale)
|
||||||
self.draw(in: rect)
|
self.draw(in: rect)
|
||||||
let newImage = UIGraphicsGetImageFromCurrentImageContext()
|
let newImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||||
UIGraphicsEndImageContext()
|
UIGraphicsEndImageContext()
|
||||||
@ -35,7 +27,7 @@ extension UIImage {
|
|||||||
- returns: The cropped image
|
- returns: The cropped image
|
||||||
*/
|
*/
|
||||||
func crop(factor: CGFloat) -> UIImage {
|
func crop(factor: CGFloat) -> UIImage {
|
||||||
let width = self.size.width * factor
|
let width = size.width * scale * factor
|
||||||
return crop(to: width)
|
return crop(to: width)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,15 +37,11 @@ extension UIImage {
|
|||||||
- returns: The cropped image
|
- returns: The cropped image
|
||||||
*/
|
*/
|
||||||
func crop(to size: CGFloat) -> UIImage {
|
func crop(to size: CGFloat) -> UIImage {
|
||||||
var rect = CGRect(
|
let rect = CGRect(
|
||||||
x: (self.size.height - size) / 2,
|
x: (self.size.height * scale - size) / 2,
|
||||||
y: (self.size.width - size) / 2,
|
y: (self.size.width * scale - size) / 2,
|
||||||
width: size,
|
width: size * scale,
|
||||||
height: size)
|
height: size * scale)
|
||||||
rect.origin.x *= scale
|
|
||||||
rect.origin.y *= scale
|
|
||||||
rect.size.width *= scale
|
|
||||||
rect.size.height *= scale
|
|
||||||
|
|
||||||
let imageRef = cgImage!.cropping(to: rect)
|
let imageRef = cgImage!.cropping(to: rect)
|
||||||
return UIImage(cgImage: imageRef!, scale: scale, orientation: imageOrientation)
|
return UIImage(cgImage: imageRef!, scale: scale, orientation: imageOrientation)
|
||||||
@ -77,7 +65,7 @@ extension UIImage {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
UIBezierPath(ovalIn: breadthRect).addClip()
|
UIBezierPath(ovalIn: breadthRect).addClip()
|
||||||
UIImage(cgImage: cgImage, scale: 1, orientation: imageOrientation).draw(in: breadthRect)
|
UIImage(cgImage: cgImage, scale: scale, orientation: imageOrientation).draw(in: breadthRect)
|
||||||
return UIGraphicsGetImageFromCurrentImageContext()
|
return UIGraphicsGetImageFromCurrentImageContext()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
//
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
//
|
|
||||||
// UIViewExtensions.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 23.03.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
extension UIView {
|
|
||||||
|
|
||||||
var recursiveSubviews: [UIView] {
|
|
||||||
var subviews = self.subviews.compactMap{ $0 }
|
|
||||||
subviews.forEach { subviews.append(contentsOf: $0.recursiveSubviews) }
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
25
Caps/Extensions/URL+Extensions.swift
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension URL {
|
||||||
|
|
||||||
|
var attributes: [FileAttributeKey : Any]? {
|
||||||
|
do {
|
||||||
|
return try FileManager.default.attributesOfItem(atPath: path)
|
||||||
|
} catch let error as NSError {
|
||||||
|
print("FileAttribute error: \(error)")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileSize: Int {
|
||||||
|
return Int(attributes?[.size] as? UInt64 ?? 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileSizeString: String {
|
||||||
|
return ByteCountFormatter.string(fromByteCount: Int64(fileSize), countStyle: .file)
|
||||||
|
}
|
||||||
|
|
||||||
|
var creationDate: Date? {
|
||||||
|
return attributes?[.creationDate] as? Date
|
||||||
|
}
|
||||||
|
}
|
@ -1,28 +0,0 @@
|
|||||||
//
|
|
||||||
// ViewControllerExtensions.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 18.03.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
extension UIViewController {
|
|
||||||
|
|
||||||
// MARK: Alerts
|
|
||||||
|
|
||||||
/// Present an alert with a message to the user
|
|
||||||
func showAlert(_ message: String, title: String = "Error") {
|
|
||||||
let alertController = UIAlertController(
|
|
||||||
title: title,
|
|
||||||
message: message,
|
|
||||||
preferredStyle: .alert)//,
|
|
||||||
//blurStyle: .dark)
|
|
||||||
|
|
||||||
alertController.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
|
|
||||||
|
|
||||||
self.present(alertController, animated: true, completion: nil)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,76 +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>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleDisplayName</key>
|
|
||||||
<string>Caps</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>$(PRODUCT_NAME)</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>$(MARKETING_VERSION)</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>1</string>
|
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
|
||||||
<array>
|
|
||||||
<string>dbapi-8-emm</string>
|
|
||||||
<string>dbapi-2</string>
|
|
||||||
</array>
|
|
||||||
<key>LSRequiresIPhoneOS</key>
|
|
||||||
<true/>
|
|
||||||
<key>NSCameraUsageDescription</key>
|
|
||||||
<string>Take images to identify matching caps and register new ones</string>
|
|
||||||
<key>UIApplicationShortcutItems</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>UIApplicationShortcutItemIconType</key>
|
|
||||||
<string>UIApplicationShortcutIconTypeCapturePhoto</string>
|
|
||||||
<key>UIApplicationShortcutItemSubtitle</key>
|
|
||||||
<string>Compare a new image</string>
|
|
||||||
<key>UIApplicationShortcutItemTitle</key>
|
|
||||||
<string>Take image</string>
|
|
||||||
<key>UIApplicationShortcutItemType</key>
|
|
||||||
<string>firstShortcut</string>
|
|
||||||
<key>UIApplicationShortcutItemUserInfo</key>
|
|
||||||
<dict>
|
|
||||||
<key>firstShortcutKey1</key>
|
|
||||||
<string>firstShortcutKeyValue1</string>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>UILaunchStoryboardName</key>
|
|
||||||
<string>LaunchScreen</string>
|
|
||||||
<key>UIMainStoryboardFile</key>
|
|
||||||
<string>Main</string>
|
|
||||||
<key>UIRequiredDeviceCapabilities</key>
|
|
||||||
<array>
|
|
||||||
<string>armv7</string>
|
|
||||||
</array>
|
|
||||||
<key>UIStatusBarStyle</key>
|
|
||||||
<string>UIStatusBarStyleDefault</string>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
</array>
|
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
|
||||||
</array>
|
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
|
||||||
<false/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
@ -1,11 +1,3 @@
|
|||||||
//
|
|
||||||
// Logger.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 11.04.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
protocol Logger {
|
protocol Logger {
|
||||||
|
@ -1,39 +0,0 @@
|
|||||||
//
|
|
||||||
// CapCell.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 22.04.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
|
|
||||||
class CapCell: UITableViewCell {
|
|
||||||
|
|
||||||
@IBOutlet private weak var capImage: RoundedImageView!
|
|
||||||
|
|
||||||
@IBOutlet private weak var matchLabel: UILabel!
|
|
||||||
|
|
||||||
@IBOutlet private weak var nameLabel: UILabel!
|
|
||||||
|
|
||||||
@IBOutlet private weak var countLabel: UILabel!
|
|
||||||
|
|
||||||
var id: Int = 0
|
|
||||||
|
|
||||||
func set(image: UIImage?) {
|
|
||||||
capImage.image = image ?? UIImage(named: "launch")
|
|
||||||
}
|
|
||||||
|
|
||||||
func set(name: String) {
|
|
||||||
self.nameLabel.text = name
|
|
||||||
}
|
|
||||||
|
|
||||||
func set(matchLabel: String) {
|
|
||||||
self.matchLabel.text = matchLabel
|
|
||||||
}
|
|
||||||
|
|
||||||
func set(countLabel: String) {
|
|
||||||
self.countLabel.text = countLabel
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,360 +0,0 @@
|
|||||||
//
|
|
||||||
// GridViewController.swift
|
|
||||||
// CapCollector
|
|
||||||
//
|
|
||||||
// Created by Christoph on 07.01.19.
|
|
||||||
// Copyright © 2019 CH. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class GridViewController: UIViewController {
|
|
||||||
|
|
||||||
/// The number of horizontal pixels for each cap.
|
|
||||||
static let len: CGFloat = 60
|
|
||||||
|
|
||||||
private lazy var rowHeight = GridViewController.len * 0.866
|
|
||||||
|
|
||||||
private lazy var margin = GridViewController.len - rowHeight
|
|
||||||
|
|
||||||
private var myView: UIView!
|
|
||||||
|
|
||||||
private var canvasSize: CGSize = .zero
|
|
||||||
|
|
||||||
@IBOutlet weak var scrollView: UIScrollView!
|
|
||||||
|
|
||||||
/// A dictionary of the caps for the tiles
|
|
||||||
private var tiles = [Int]()
|
|
||||||
|
|
||||||
/// The name of the tile image
|
|
||||||
private var name: String = "default"
|
|
||||||
|
|
||||||
/// The number of caps horizontally.
|
|
||||||
private var columns = 40
|
|
||||||
|
|
||||||
/// A dictionary for the colors of the caps
|
|
||||||
private var colors = [Int : UIColor]()
|
|
||||||
|
|
||||||
/// The currently displaxed image views indexed by their tile ids
|
|
||||||
private var installedTiles = [Int : RoundedImageView]()
|
|
||||||
|
|
||||||
private var selectedTile: Int? = nil
|
|
||||||
|
|
||||||
private weak var selectionView: RoundedButton!
|
|
||||||
|
|
||||||
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
|
|
||||||
return .portrait
|
|
||||||
}
|
|
||||||
|
|
||||||
override var shouldAutorotate: Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private var isShowingColors = false
|
|
||||||
|
|
||||||
@IBAction func toggleAverageColor(_ sender: Any) {
|
|
||||||
isShowingColors = !isShowingColors
|
|
||||||
for (tile, view) in installedTiles {
|
|
||||||
if isShowingColors {
|
|
||||||
view.image = nil
|
|
||||||
view.backgroundColor = tileColor(tile: tile)
|
|
||||||
} else {
|
|
||||||
let id = tiles[tile]
|
|
||||||
if let image = app.database.storage.thumbnail(for: id) {
|
|
||||||
view.image = image
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
self.downloadImage(cap: id, tile: tile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func load(tileImage: Database.TileImage) {
|
|
||||||
let totalCount = app.database.capCount
|
|
||||||
let firstNewId = tileImage.caps.count + 1
|
|
||||||
if totalCount >= firstNewId {
|
|
||||||
self.tiles = tileImage.caps + (firstNewId...totalCount)
|
|
||||||
} else {
|
|
||||||
self.tiles = tileImage.caps
|
|
||||||
}
|
|
||||||
self.columns = tileImage.width
|
|
||||||
self.name = tileImage.name
|
|
||||||
}
|
|
||||||
|
|
||||||
private func saveTileImage() {
|
|
||||||
let tileImage = Database.TileImage(name: name, width: columns, caps: tiles)
|
|
||||||
guard app.database.save(tileImage: tileImage) else {
|
|
||||||
log("Failed to save tile image")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log("Tile image saved")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
colors = app.database.colors
|
|
||||||
|
|
||||||
let width = CGFloat(columns) * GridViewController.len + GridViewController.len / 2
|
|
||||||
let height = (CGFloat(tiles.count) / CGFloat(columns)).rounded(.up) * rowHeight + margin
|
|
||||||
canvasSize = CGSize(width: width, height: height)
|
|
||||||
myView = UIView(frame: CGRect(origin: .zero, size: canvasSize))
|
|
||||||
|
|
||||||
scrollView.addSubview(myView)
|
|
||||||
scrollView.contentSize = canvasSize
|
|
||||||
scrollView.delegate = self
|
|
||||||
scrollView.zoomScale = 0.5
|
|
||||||
scrollView.maximumZoomScale = 1
|
|
||||||
setZoomRange()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
|
||||||
super.viewWillAppear(animated)
|
|
||||||
|
|
||||||
updateTiles()
|
|
||||||
|
|
||||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
|
|
||||||
myView.addGestureRecognizer(tapRecognizer)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
|
||||||
setZoomRange()
|
|
||||||
updateTiles()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
|
||||||
super.viewWillDisappear(animated)
|
|
||||||
|
|
||||||
saveTileImage()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Tiles
|
|
||||||
|
|
||||||
private func tileColor(tile: Int) -> UIColor? {
|
|
||||||
let id = tiles[tile]
|
|
||||||
return colors[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Switch two tiles.
|
|
||||||
*/
|
|
||||||
private func switchTiles(_ lhs: Int, _ rhs: Int) -> Bool {
|
|
||||||
let temp = tiles[rhs]
|
|
||||||
tiles[rhs] = tiles[lhs]
|
|
||||||
tiles[lhs] = temp
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private func setZoomRange() {
|
|
||||||
let size = scrollView.frame.size
|
|
||||||
let a = size.width / canvasSize.width
|
|
||||||
let b = size.height / canvasSize.height
|
|
||||||
let scale = min(a,b)
|
|
||||||
scrollView.minimumZoomScale = min(a,b)
|
|
||||||
if scrollView.zoomScale < scale {
|
|
||||||
scrollView.setZoomScale(scale, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func handleTap(_ sender: UITapGestureRecognizer) {
|
|
||||||
|
|
||||||
let loc = sender.location(in: myView)
|
|
||||||
let y = loc.y
|
|
||||||
let s = y.truncatingRemainder(dividingBy: rowHeight)
|
|
||||||
let row = Int(y / rowHeight)
|
|
||||||
guard s > margin else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let column: Int
|
|
||||||
if row.isEven {
|
|
||||||
column = Int(loc.x / GridViewController.len)
|
|
||||||
// Abort, if user tapped outside of the grid
|
|
||||||
if column >= columns {
|
|
||||||
clearTileSelection()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
column = Int((loc.x - GridViewController.len / 2) / GridViewController.len)
|
|
||||||
}
|
|
||||||
handleTileTapped(tile: row * columns + Int(column))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handleTileTapped(tile: Int) {
|
|
||||||
if let selected = selectedTile {
|
|
||||||
switchTiles(oldTile: selected, newTile: tile)
|
|
||||||
} else {
|
|
||||||
showSelection(tile: tile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func showSelection(tile: Int) {
|
|
||||||
clearTileSelection()
|
|
||||||
|
|
||||||
if let view = installedTiles[tile] {
|
|
||||||
view.borderWidth = 3
|
|
||||||
view.borderColor = AppDelegate.tintColor
|
|
||||||
selectedTile = tile
|
|
||||||
} else {
|
|
||||||
selectedTile = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func tileIsVisible(tile: Int, in rect: CGRect) -> Bool {
|
|
||||||
return rect.intersects(frame(for: tile))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func makeTile(_ tile: Int) {
|
|
||||||
let view = RoundedImageView(frame: frame(for: tile))
|
|
||||||
myView.addSubview(view)
|
|
||||||
defer {
|
|
||||||
installedTiles[tile] = view
|
|
||||||
}
|
|
||||||
// Only set image if images are shown
|
|
||||||
guard !isShowingColors else {
|
|
||||||
view.backgroundColor = tileColor(tile: tile)
|
|
||||||
return
|
|
||||||
|
|
||||||
}
|
|
||||||
if let image = app.database.storage.thumbnail(for: tiles[tile]) {
|
|
||||||
view.image = image
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadImage(tile: tile)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func downloadImage(tile: Int) {
|
|
||||||
let id = tiles[tile]
|
|
||||||
downloadImage(cap: id, tile: tile)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func downloadImage(cap id: Int, tile: Int) {
|
|
||||||
app.database.downloadImage(for: id) { img in
|
|
||||||
guard img != nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let view = self.installedTiles[tile] else {
|
|
||||||
self.log("No installed tile for downloaded image \(id)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let image = app.database.storage.thumbnail(for: id) else {
|
|
||||||
self.log("Failed to load image for cap \(id) after successful download")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
guard self.isShowingColors else {
|
|
||||||
view.image = image
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let color = image.averageColor else {
|
|
||||||
self.log("Failed to get average color from image for cap \(id)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
view.backgroundColor = color
|
|
||||||
self.colors[id] = color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func frame(for tile: Int) -> CGRect {
|
|
||||||
let row = tile / columns
|
|
||||||
let column = tile - row * columns
|
|
||||||
let x = CGFloat(column) * GridViewController.len + (row.isEven ? 0 : GridViewController.len / 2)
|
|
||||||
let y = CGFloat(row) * rowHeight
|
|
||||||
return CGRect(x: x, y: y, width: GridViewController.len, height: GridViewController.len)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func switchTiles(oldTile: Int, newTile: Int) {
|
|
||||||
guard oldTile != newTile else {
|
|
||||||
clearTileSelection()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard switchTiles(oldTile, newTile) else {
|
|
||||||
clearTileSelection()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Switch cap colors
|
|
||||||
let temp = installedTiles[oldTile]?.backgroundColor
|
|
||||||
installedTiles[oldTile]?.backgroundColor = installedTiles[newTile]?.backgroundColor
|
|
||||||
installedTiles[newTile]?.backgroundColor = temp
|
|
||||||
if !isShowingColors {
|
|
||||||
let temp = installedTiles[oldTile]?.image
|
|
||||||
installedTiles[oldTile]?.image = installedTiles[newTile]?.image
|
|
||||||
installedTiles[newTile]?.image = temp
|
|
||||||
}
|
|
||||||
clearTileSelection()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private func clearTileSelection() {
|
|
||||||
guard let tile = selectedTile else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
installedTiles[tile]?.borderWidth = 0
|
|
||||||
selectedTile = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func showTiles(in rect: CGRect) {
|
|
||||||
for tile in 0..<tiles.count {
|
|
||||||
refresh(tile: tile, inVisibleRect: rect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func refresh(tile: Int, inVisibleRect rect: CGRect) {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
makeTile(tile)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func remove(tile: Int) {
|
|
||||||
installedTiles[tile]?.removeFromSuperview()
|
|
||||||
installedTiles[tile] = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private var visibleRect: CGRect {
|
|
||||||
let scale = scrollView.zoomScale
|
|
||||||
let offset = scrollView.contentOffset
|
|
||||||
let size = scrollView.visibleSize
|
|
||||||
|
|
||||||
let scaledOrigin = CGPoint(x: offset.x / scale, y: offset.y / scale)
|
|
||||||
let scaledSize = CGSize(width: size.width / scale, height: size.height / scale)
|
|
||||||
return CGRect(origin: scaledOrigin, size: scaledSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateTiles() {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.showTiles(in: self.visibleRect)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension GridViewController: UIScrollViewDelegate {
|
|
||||||
|
|
||||||
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
|
||||||
return myView
|
|
||||||
}
|
|
||||||
|
|
||||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
||||||
updateTiles()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private extension Int {
|
|
||||||
|
|
||||||
var isEven: Bool {
|
|
||||||
return self % 2 == 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension GridViewController: Logger { }
|
|
@ -1,15 +0,0 @@
|
|||||||
//
|
|
||||||
// ImageCell.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 07.02.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class ImageCell: UICollectionViewCell {
|
|
||||||
|
|
||||||
@IBOutlet weak var capView: UIImageView!
|
|
||||||
|
|
||||||
}
|
|
@ -1,186 +0,0 @@
|
|||||||
//
|
|
||||||
// ListViewController.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 22.02.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class ImageSelector: UIViewController {
|
|
||||||
|
|
||||||
// MARK: - Constants
|
|
||||||
|
|
||||||
/// The number of items per row
|
|
||||||
private let itemsPerRow: CGFloat = 3
|
|
||||||
|
|
||||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
|
||||||
return .portrait
|
|
||||||
}
|
|
||||||
|
|
||||||
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
|
|
||||||
return .portrait
|
|
||||||
}
|
|
||||||
|
|
||||||
override var shouldAutorotate: Bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Variables
|
|
||||||
|
|
||||||
private var titleLabel: UILabel!
|
|
||||||
|
|
||||||
private var subtitleLabel: UILabel!
|
|
||||||
|
|
||||||
private var images = [UIImage?]()
|
|
||||||
|
|
||||||
var cap: Cap!
|
|
||||||
|
|
||||||
weak var imageProvider: ImageProvider?
|
|
||||||
|
|
||||||
@IBOutlet weak var collection: UICollectionView!
|
|
||||||
|
|
||||||
private var titleText: String {
|
|
||||||
"Cap \(cap.id) (\(cap.count) images)"
|
|
||||||
}
|
|
||||||
|
|
||||||
private var subtitleText: String {
|
|
||||||
cap.name
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Life cycle
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
collection.dataSource = self
|
|
||||||
collection.delegate = self
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
|
||||||
super.viewWillAppear(animated)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
private func downloadImages() {
|
|
||||||
images = [UIImage?](repeating: nil, count: cap.count)
|
|
||||||
log("\(cap.count) images for cap \(cap.id)")
|
|
||||||
for version in 0..<cap.count {
|
|
||||||
if let image = imageProvider?.image(for: cap.id, version: version) {
|
|
||||||
log("Image \(version) already downloaded")
|
|
||||||
set(image, for: version)
|
|
||||||
} else {
|
|
||||||
log("Downloading image \(version)")
|
|
||||||
app.database.downloadImage(for: cap.id, version: version) { image in
|
|
||||||
self.set(image, for: version)
|
|
||||||
if image != nil {
|
|
||||||
self.log("Downloaded version \(version)")
|
|
||||||
} else {
|
|
||||||
self.log("Failed to download image \(version)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func set(_ image: UIImage?, for version: Int) {
|
|
||||||
self.images[version] = image
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.collection.reloadItems(at: [IndexPath(row: version, section: 0)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Select
|
|
||||||
|
|
||||||
private func selectedImage(nr: Int) {
|
|
||||||
app.database.setMainImage(of: cap.id, to: nr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UICollectionViewDataSource
|
|
||||||
|
|
||||||
extension ImageSelector: UICollectionViewDataSource {
|
|
||||||
|
|
||||||
func numberOfSections(in collectionView: UICollectionView) -> Int {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
|
||||||
return images.count
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
|
||||||
let cell = collectionView.dequeueReusableCell(
|
|
||||||
withReuseIdentifier: "Image",
|
|
||||||
for: indexPath) as! ImageCell
|
|
||||||
|
|
||||||
cell.capView.image = images[indexPath.row] ?? UIImage(named: "launch")
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
|
||||||
selectedImage(nr: indexPath.row)
|
|
||||||
navigationController?.popViewController(animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UICollectionViewDelegateFlowLayout
|
|
||||||
|
|
||||||
extension ImageSelector : UICollectionViewDelegateFlowLayout {
|
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView,
|
|
||||||
layout collectionViewLayout: UICollectionViewLayout,
|
|
||||||
sizeForItemAt indexPath: IndexPath) -> CGSize {
|
|
||||||
let widthPerItem = collectionView.frame.width / itemsPerRow
|
|
||||||
return CGSize(width: widthPerItem, height: widthPerItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView,
|
|
||||||
layout collectionViewLayout: UICollectionViewLayout,
|
|
||||||
insetForSectionAt section: Int) -> UIEdgeInsets {
|
|
||||||
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func collectionView(_ collectionView: UICollectionView,
|
|
||||||
layout collectionViewLayout: UICollectionViewLayout,
|
|
||||||
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ImageSelector: Logger { }
|
|
||||||
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
|||||||
//
|
|
||||||
// NavigationController.swift
|
|
||||||
// CapCollector
|
|
||||||
//
|
|
||||||
// Created by Christoph on 08.01.19.
|
|
||||||
// Copyright © 2019 CH. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class NavigationController: UINavigationController {
|
|
||||||
|
|
||||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
|
||||||
return allowLandscape ? .allButUpsideDown : .portrait
|
|
||||||
}
|
|
||||||
|
|
||||||
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
|
|
||||||
return .portrait
|
|
||||||
}
|
|
||||||
|
|
||||||
override var shouldAutorotate: Bool {
|
|
||||||
return allowLandscape
|
|
||||||
}
|
|
||||||
|
|
||||||
var allowLandscape: Bool = false
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
// Do any additional setup after loading the view.
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,161 +0,0 @@
|
|||||||
//
|
|
||||||
// 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: AnyObject {
|
|
||||||
|
|
||||||
func capSearchWasDismissed()
|
|
||||||
|
|
||||||
func capSearch(didChange text: String)
|
|
||||||
|
|
||||||
func capAccessoryDidDiscardImage()
|
|
||||||
|
|
||||||
func capAccessory(shouldSave image: UIImage)
|
|
||||||
|
|
||||||
func capAccessoryCameraButtonPressed()
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchAndDisplayAccessory: PassthroughView {
|
|
||||||
|
|
||||||
// MARK: - Outlets
|
|
||||||
|
|
||||||
@IBOutlet weak var capImage: RoundedImageView!
|
|
||||||
|
|
||||||
@IBOutlet weak var saveButton: UIButton!
|
|
||||||
|
|
||||||
@IBOutlet weak var cameraButton: UIButton!
|
|
||||||
|
|
||||||
@IBOutlet weak var searchBar: UISearchBar!
|
|
||||||
|
|
||||||
@IBOutlet weak var imageHeightContraint: NSLayoutConstraint!
|
|
||||||
|
|
||||||
// MARK: - Actions
|
|
||||||
|
|
||||||
@IBAction func cameraButtonPressed() {
|
|
||||||
if isShowingCapImage {
|
|
||||||
discardImage()
|
|
||||||
} else {
|
|
||||||
delegate?.capAccessoryCameraButtonPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func saveButtonPressed() {
|
|
||||||
if let image = capImage.image {
|
|
||||||
delegate?.capAccessory(shouldSave: image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Variables
|
|
||||||
|
|
||||||
var view: UIView?
|
|
||||||
|
|
||||||
weak var delegate: CapAccessoryDelegate?
|
|
||||||
|
|
||||||
var currentImage: UIImage? {
|
|
||||||
capImage.image
|
|
||||||
}
|
|
||||||
|
|
||||||
var isShowingCapImage: Bool {
|
|
||||||
capImage.image != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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!)
|
|
||||||
|
|
||||||
hideImageView()
|
|
||||||
|
|
||||||
searchBar.text = nil
|
|
||||||
searchBar.setShowsCancelButton(false, animated: false)
|
|
||||||
searchBar.delegate = self
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Search bar
|
|
||||||
|
|
||||||
func dismissAndClearSearchBar() {
|
|
||||||
searchBar.resignFirstResponder()
|
|
||||||
searchBar.text = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Cap image
|
|
||||||
|
|
||||||
func showImageView(with image: UIImage) {
|
|
||||||
capImage.image = image
|
|
||||||
cameraButton.setImage(UIImage(systemName: "xmark"), for: .normal)
|
|
||||||
|
|
||||||
imageHeightContraint.constant = 90
|
|
||||||
capImage.alpha = 1
|
|
||||||
capImage.isHidden = false
|
|
||||||
saveButton.isHidden = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func discardImage() {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.dismissAndClearSearchBar()
|
|
||||||
self.hideImageView()
|
|
||||||
}
|
|
||||||
delegate?.capAccessoryDidDiscardImage()
|
|
||||||
}
|
|
||||||
|
|
||||||
func hideImageView() {
|
|
||||||
capImage.image = nil
|
|
||||||
cameraButton.setImage(UIImage(systemName: "camera"), for: .normal)
|
|
||||||
|
|
||||||
//imageHeightContraint.constant = 0
|
|
||||||
capImage.alpha = 0
|
|
||||||
capImage.isHidden = true
|
|
||||||
saveButton.isHidden = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
|
||||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
|
||||||
<dependencies>
|
|
||||||
<deployment identifier="iOS"/>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17125"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
|
||||||
<capability name="System colors in document resources" minToolsVersion="11.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="imageHeightContraint" destination="Phy-Uy-08W" id="gc0-1X-MIz"/>
|
|
||||||
<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>
|
|
||||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="B0c-fn-aK3">
|
|
||||||
<rect key="frame" x="0.0" y="90" width="414" height="55"/>
|
|
||||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="Arf-oz-vtV">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="414" height="55"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
</view>
|
|
||||||
<blurEffect style="regular"/>
|
|
||||||
</visualEffectView>
|
|
||||||
<searchBar contentMode="redraw" searchBarStyle="minimal" placeholder="Search caps" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bCh-7y-t0w">
|
|
||||||
<rect key="frame" x="0.0" y="90" width="359" height="55"/>
|
|
||||||
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" returnKeyType="search" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no"/>
|
|
||||||
<scopeButtonTitles>
|
|
||||||
<string>Title</string>
|
|
||||||
<string>Title</string>
|
|
||||||
</scopeButtonTitles>
|
|
||||||
</searchBar>
|
|
||||||
<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="5" y="0.0" width="90" height="90"/>
|
|
||||||
<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"/>
|
|
||||||
</userDefinedRuntimeAttribute>
|
|
||||||
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
|
|
||||||
<real key="value" value="1"/>
|
|
||||||
</userDefinedRuntimeAttribute>
|
|
||||||
</userDefinedRuntimeAttributes>
|
|
||||||
</imageView>
|
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dt5-LD-28a" userLabel="Save Button">
|
|
||||||
<rect key="frame" x="5" y="0.0" width="90" height="90"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" constant="90" id="Phy-Uy-08W"/>
|
|
||||||
</constraints>
|
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
|
||||||
<connections>
|
|
||||||
<action selector="saveButtonPressed" destination="-1" eventType="touchUpInside" id="O49-6L-mNY"/>
|
|
||||||
</connections>
|
|
||||||
</button>
|
|
||||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="bIA-eq-Tn5" userLabel="Camera/Clear Button">
|
|
||||||
<rect key="frame" x="359" y="90" width="55" height="55"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="width" secondItem="bIA-eq-Tn5" secondAttribute="height" multiplier="1:1" id="O09-ww-bHE"/>
|
|
||||||
</constraints>
|
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle0"/>
|
|
||||||
<state key="normal" image="camera" catalog="system">
|
|
||||||
<preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="large"/>
|
|
||||||
</state>
|
|
||||||
<connections>
|
|
||||||
<action selector="cameraButtonPressed" destination="-1" eventType="touchUpInside" id="ooo-b8-Atj"/>
|
|
||||||
</connections>
|
|
||||||
</button>
|
|
||||||
</subviews>
|
|
||||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="dt5-LD-28a" firstAttribute="top" secondItem="vQm-nH-J8o" secondAttribute="top" id="0fL-ow-Unw"/>
|
|
||||||
<constraint firstItem="dt5-LD-28a" firstAttribute="leading" secondItem="vQm-nH-J8o" secondAttribute="leading" id="BDw-lR-hYd"/>
|
|
||||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="B0c-fn-aK3" secondAttribute="bottom" id="Cbb-yb-0av"/>
|
|
||||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="bIA-eq-Tn5" secondAttribute="bottom" id="Geo-F0-pdI"/>
|
|
||||||
<constraint firstItem="bIA-eq-Tn5" firstAttribute="top" secondItem="bCh-7y-t0w" secondAttribute="top" id="IiT-eG-qfb"/>
|
|
||||||
<constraint firstItem="vQm-nH-J8o" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="5" id="Kf4-2d-yxI"/>
|
|
||||||
<constraint firstItem="bCh-7y-t0w" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="M9M-He-vxM"/>
|
|
||||||
<constraint firstItem="vQm-nH-J8o" firstAttribute="top" secondItem="vUN-kp-3ea" secondAttribute="top" id="PPc-Zp-Vty"/>
|
|
||||||
<constraint firstItem="B0c-fn-aK3" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="Pp8-wt-rM7"/>
|
|
||||||
<constraint firstItem="dt5-LD-28a" firstAttribute="bottom" secondItem="vQm-nH-J8o" secondAttribute="bottom" id="VD3-fH-dsS"/>
|
|
||||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="B0c-fn-aK3" secondAttribute="trailing" id="bri-jC-TDo"/>
|
|
||||||
<constraint firstItem="dt5-LD-28a" firstAttribute="trailing" secondItem="vQm-nH-J8o" secondAttribute="trailing" id="eg2-RT-iVc"/>
|
|
||||||
<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="bIA-eq-Tn5" secondAttribute="trailing" id="kQV-mX-EoD"/>
|
|
||||||
<constraint firstItem="B0c-fn-aK3" firstAttribute="top" secondItem="bCh-7y-t0w" secondAttribute="top" id="l7d-ZC-pNp"/>
|
|
||||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="bCh-7y-t0w" secondAttribute="bottom" id="r8c-kO-KT5"/>
|
|
||||||
<constraint firstItem="bCh-7y-t0w" firstAttribute="top" secondItem="vQm-nH-J8o" secondAttribute="bottom" id="tBl-Ig-g9a"/>
|
|
||||||
</constraints>
|
|
||||||
<nil key="simulatedTopBarMetrics"/>
|
|
||||||
<nil key="simulatedBottomBarMetrics"/>
|
|
||||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
|
||||||
<point key="canvasLocation" x="96" y="104.7976011994003"/>
|
|
||||||
</view>
|
|
||||||
</objects>
|
|
||||||
<resources>
|
|
||||||
<image name="camera" catalog="system" width="128" height="94"/>
|
|
||||||
<systemColor name="secondaryLabelColor">
|
|
||||||
<color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
|
||||||
</systemColor>
|
|
||||||
</resources>
|
|
||||||
</document>
|
|
@ -1,113 +0,0 @@
|
|||||||
//
|
|
||||||
// SortController.swift
|
|
||||||
// CapCollector
|
|
||||||
//
|
|
||||||
// Created by Christoph on 12.11.18.
|
|
||||||
// Copyright © 2018 CH. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
enum SortCriteria: Int {
|
|
||||||
case id = 0
|
|
||||||
case name = 1
|
|
||||||
case count = 2
|
|
||||||
case match = 3
|
|
||||||
|
|
||||||
var text: String {
|
|
||||||
switch self {
|
|
||||||
case .id:
|
|
||||||
return "Id"
|
|
||||||
case .name:
|
|
||||||
return "Name"
|
|
||||||
case .count:
|
|
||||||
return "Count"
|
|
||||||
case .match:
|
|
||||||
return "Match"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol SortControllerDelegate: AnyObject {
|
|
||||||
|
|
||||||
func sortController(didSelect sortType: SortCriteria, ascending: Bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
class SortController: UITableViewController {
|
|
||||||
|
|
||||||
@IBOutlet weak var thirdRowLabel: UILabel!
|
|
||||||
|
|
||||||
var selected: SortCriteria = .id
|
|
||||||
|
|
||||||
var ascending: Bool = false
|
|
||||||
|
|
||||||
weak var delegate: SortControllerDelegate?
|
|
||||||
|
|
||||||
var options = [SortCriteria]()
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
preferredContentSize = CGSize(width: 200, height: 139 + options.count * 40)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func giveFeedback(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
|
|
||||||
UIImpactFeedbackGenerator(style: style).impactOccurred()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func sortCriteria(for index: Int) -> SortCriteria {
|
|
||||||
index < options.count ? options[index] : .match
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Table view data source
|
|
||||||
|
|
||||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
|
||||||
section == 0 ? "Sort order" : "Sort by"
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
||||||
section == 0 ? 1 : options.count
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "SortCell")!
|
|
||||||
|
|
||||||
guard indexPath.section != 0 else {
|
|
||||||
cell.accessoryType = ascending ? .checkmark : .none
|
|
||||||
cell.textLabel?.text = "Ascending"
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
let select = sortCriteria(for: indexPath.row)
|
|
||||||
cell.textLabel?.text = select.text
|
|
||||||
guard select == selected else {
|
|
||||||
cell.accessoryType = .none
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
cell.accessoryType = .checkmark
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
||||||
40
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
||||||
tableView.deselectRow(at: indexPath, animated: true)
|
|
||||||
guard indexPath.section == 1 else {
|
|
||||||
ascending = !ascending
|
|
||||||
tableView.reloadRows(at: [indexPath], with: .automatic)
|
|
||||||
delegate?.sortController(didSelect: selected, ascending: ascending)
|
|
||||||
giveFeedback(.light)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
giveFeedback(.medium)
|
|
||||||
selected = sortCriteria(for: indexPath.row)
|
|
||||||
tableView.reloadRows(at: [indexPath], with: .automatic)
|
|
||||||
delegate?.sortController(didSelect: selected, ascending: ascending)
|
|
||||||
self.dismiss(animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
@ -1,885 +0,0 @@
|
|||||||
//
|
|
||||||
// TableView.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 22.04.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
enum NavigationBarDataType {
|
|
||||||
|
|
||||||
case appInfo
|
|
||||||
case upload
|
|
||||||
case thumbnails
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol NavigationBarDataSource {
|
|
||||||
|
|
||||||
var title: String { get }
|
|
||||||
|
|
||||||
var subtitle: String { get }
|
|
||||||
|
|
||||||
var id: NavigationBarDataType { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
class TableView: UITableViewController {
|
|
||||||
|
|
||||||
@IBOutlet weak var infoButton: UIBarButtonItem!
|
|
||||||
|
|
||||||
private lazy var classifier: Classifier? = loadClassifier()
|
|
||||||
|
|
||||||
private var accessory: SearchAndDisplayAccessory?
|
|
||||||
|
|
||||||
private var titleLabel: UILabel!
|
|
||||||
|
|
||||||
private var subtitleLabel: UILabel!
|
|
||||||
|
|
||||||
private var caps = [Cap]()
|
|
||||||
|
|
||||||
private var shownCaps = [Cap]()
|
|
||||||
|
|
||||||
private var matches: [Int : Float]?
|
|
||||||
|
|
||||||
private var sortType: SortCriteria = .id
|
|
||||||
|
|
||||||
private var searchText: String? = nil
|
|
||||||
|
|
||||||
private var sortAscending: Bool = false
|
|
||||||
|
|
||||||
/// This will be set to a cap id when adding a cap to it
|
|
||||||
private var capToAddImageTo: Int?
|
|
||||||
|
|
||||||
private var isUnlocked = false
|
|
||||||
|
|
||||||
var imageProvider: ImageProvider {
|
|
||||||
app.database.storage
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Computed properties
|
|
||||||
|
|
||||||
private var titleText: String {
|
|
||||||
let recognized = app.database.recognizedCapCount
|
|
||||||
let all = app.database.capCount
|
|
||||||
switch all {
|
|
||||||
case 0:
|
|
||||||
return "No caps"
|
|
||||||
case 1:
|
|
||||||
return "1 cap"
|
|
||||||
case recognized:
|
|
||||||
return "\(all) caps"
|
|
||||||
default:
|
|
||||||
return "\(all) caps (\(all - recognized) new)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var subtitleText: String {
|
|
||||||
let capCount = app.database.capCount
|
|
||||||
guard capCount > 0, isUnlocked else {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
let allImages = app.database.imageCount
|
|
||||||
|
|
||||||
let ratio = Float(allImages) / Float(capCount)
|
|
||||||
return String(format: "%d images (%.2f per cap)", allImages, ratio)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Overrides
|
|
||||||
|
|
||||||
override var inputAccessoryView: UIView? {
|
|
||||||
get { return accessory }
|
|
||||||
}
|
|
||||||
|
|
||||||
override var canBecomeFirstResponder: Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Actions
|
|
||||||
|
|
||||||
@IBAction func updateInfo(_ sender: UIBarButtonItem, forEvent event: UIEvent) {
|
|
||||||
guard let touch = event.allTouches?.first, touch.tapCount > 0 else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard !app.database.isInOfflineMode else {
|
|
||||||
showOfflineDialog()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
app.database.startInitialDownload()
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func showMosaic(_ sender: UIBarButtonItem) {
|
|
||||||
checkThumbnailsAndColorsBeforShowingGrid()
|
|
||||||
}
|
|
||||||
|
|
||||||
func showCameraView() {
|
|
||||||
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
|
||||||
let controller = storyboard.instantiateViewController(withIdentifier: "NewImageController") as! CameraController
|
|
||||||
controller.delegate = self
|
|
||||||
self.present(controller, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func titleWasTapped() {
|
|
||||||
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
|
||||||
let controller = storyboard.instantiateViewController(withIdentifier: "SortController") as! SortController
|
|
||||||
controller.selected = sortType
|
|
||||||
controller.ascending = sortAscending
|
|
||||||
controller.delegate = self
|
|
||||||
|
|
||||||
controller.options = [.id, .name]
|
|
||||||
if isUnlocked { controller.options.append(.count) }
|
|
||||||
if matches != nil { controller.options.append(.match) }
|
|
||||||
|
|
||||||
let presentationController = AlwaysPresentAsPopover.configurePresentation(forController: controller)
|
|
||||||
|
|
||||||
presentationController.sourceView = navigationItem.titleView!
|
|
||||||
presentationController.permittedArrowDirections = [.up]
|
|
||||||
self.present(controller, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Life cycle
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
tableView.rowHeight = 100
|
|
||||||
|
|
||||||
accessory = SearchAndDisplayAccessory(width: self.view.frame.width)
|
|
||||||
accessory?.delegate = self
|
|
||||||
|
|
||||||
initInfoButton()
|
|
||||||
|
|
||||||
app.database.delegate = self
|
|
||||||
let count = app.database.capCount
|
|
||||||
if count == 0 {
|
|
||||||
log("No caps found, downloading names")
|
|
||||||
app.database.startInitialDownload()
|
|
||||||
} else {
|
|
||||||
log("Loaded \(count) caps")
|
|
||||||
reloadCapsFromDatabase()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
|
||||||
super.viewWillAppear(animated)
|
|
||||||
|
|
||||||
(navigationController as? NavigationController)?.allowLandscape = false
|
|
||||||
isUnlocked = app.isUnlocked
|
|
||||||
log(isUnlocked ? "App is unlocked" : "App is locked")
|
|
||||||
app.database.startBackgroundWork()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func didMove(toParent parent: UIViewController?) {
|
|
||||||
super.didMove(toParent: parent)
|
|
||||||
|
|
||||||
guard parent != nil && self.navigationItem.titleView == nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
initNavigationItemTitleView()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func initInfoButton() {
|
|
||||||
let offline = app.database.isInOfflineMode
|
|
||||||
setInfoButtonIcon(offline: offline)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setInfoButtonIcon(offline: Bool) {
|
|
||||||
let symbol = offline ? "icloud.slash" : "arrow.clockwise.icloud"
|
|
||||||
infoButton.image = UIImage(systemName: symbol)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
let recognizer = UITapGestureRecognizer(target: self, action: #selector(titleWasTapped))
|
|
||||||
stackView.isUserInteractionEnabled = true
|
|
||||||
stackView.addGestureRecognizer(recognizer)
|
|
||||||
|
|
||||||
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(attemptChangeOfUserPermissions))
|
|
||||||
stackView.addGestureRecognizer(longPress)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func set(title: String, subtitle: String) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.titleLabel?.text = title
|
|
||||||
self.subtitleLabel?.text = subtitle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateNavigationItemTitleView() {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.titleLabel?.text = self.titleText
|
|
||||||
self.subtitleLabel?.text = self.subtitleText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Starting updates
|
|
||||||
|
|
||||||
private func checkThumbnailsAndColorsBeforShowingGrid() {
|
|
||||||
let colors = app.database.pendingCapsForColorCreation
|
|
||||||
let thumbs = app.database.pendingCapForThumbnailCreation
|
|
||||||
guard colors == 0 && thumbs == 0 else {
|
|
||||||
app.database.startBackgroundWork()
|
|
||||||
showAlert("Please wait until all background work is completed. \(colors) colors and \(thumbs) thumbnails need to be created.", title: "Mosaic not ready")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
showGrid()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func showGrid() {
|
|
||||||
let vc = app.mainStoryboard.instantiateViewController(withIdentifier: "GridView") as! GridViewController
|
|
||||||
guard let nav = navigationController as? NavigationController else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if let tileImage = app.database.tileImage(named: "default") {
|
|
||||||
log("Showing existing tile image")
|
|
||||||
vc.load(tileImage: tileImage)
|
|
||||||
} else {
|
|
||||||
let tileImage = Database.TileImage(name: "default", width: 40, caps: [])
|
|
||||||
log("Showing default tile image")
|
|
||||||
vc.load(tileImage: tileImage)
|
|
||||||
}
|
|
||||||
nav.pushViewController(vc, animated: true)
|
|
||||||
nav.allowLandscape = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private func showOfflineDialog() {
|
|
||||||
let offline = app.database.isInOfflineMode
|
|
||||||
if offline {
|
|
||||||
print("Marking as online")
|
|
||||||
app.database.isInOfflineMode = false
|
|
||||||
app.database.startBackgroundWork()
|
|
||||||
self.showAlert("Offline mode was disabled", title: "Online")
|
|
||||||
} else {
|
|
||||||
print("Marking as offline")
|
|
||||||
app.database.isInOfflineMode = true
|
|
||||||
self.showAlert("Offline mode was enabled", title: "Offline")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func rename(cap: Cap, at indexPath: IndexPath) {
|
|
||||||
let detail = "Choose a new name for the cap"
|
|
||||||
askUserForText("Enter new name", detail: detail, existingText: cap.name, yesText: "Save") { text in
|
|
||||||
guard app.database.update(name: text, for: cap.id) else {
|
|
||||||
self.showAlert("Name could not be set.", title: "Update failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func saveNewCap(for image: UIImage) {
|
|
||||||
let detail = "Choose a name for the image"
|
|
||||||
askUserForText("Enter name", detail: detail, existingText: accessory!.searchBar.text, yesText: "Save") { text in
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
|
||||||
guard app.database.createCap(image: image, name: text) else {
|
|
||||||
self.showAlert("Cap not added", title: "Database error")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.accessory!.discardImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func updateShownCaps(_ newList: [Cap], insertedId id: Int) {
|
|
||||||
// Main queue
|
|
||||||
guard shownCaps.count == newList.count - 1 else {
|
|
||||||
log("Cap list refresh mismatch: was \(shownCaps.count), is \(newList.count)")
|
|
||||||
show(sortedCaps: newList)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let index = newList.firstIndex(where: { $0.id == id}) else {
|
|
||||||
log("Cap list refresh without new cap \(id)")
|
|
||||||
show(sortedCaps: newList)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.tableView.beginUpdates()
|
|
||||||
self.shownCaps = newList
|
|
||||||
let indexPath = IndexPath(row: index, section: 0)
|
|
||||||
self.tableView.insertRows(at: [indexPath], with: .automatic)
|
|
||||||
self.tableView.endUpdates()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: User interaction
|
|
||||||
|
|
||||||
@objc private func attemptChangeOfUserPermissions() {
|
|
||||||
guard isUnlocked else {
|
|
||||||
attemptAppUnlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log("Locking app.")
|
|
||||||
app.lock()
|
|
||||||
isUnlocked = false
|
|
||||||
showAllCapsAndScrollToTop()
|
|
||||||
updateNavigationItemTitleView()
|
|
||||||
showAlert("The app was locked to prevent modifications.", title: "Locked")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func attemptAppUnlock() {
|
|
||||||
log("Presenting unlock dialog to user")
|
|
||||||
askUserForText("Enter pin", detail: "Enter the correct pin to unlock write permissions for the app.", placeholder: "Pin", yesText: "Unlock") { text in
|
|
||||||
guard let pin = Int(text), app.checkUnlock(with: pin) else {
|
|
||||||
self.unlockFailed()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.unlockDidSucceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func unlockFailed() {
|
|
||||||
showAlert("The pin you entered is incorrect.", title: "Invalid pin")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func unlockDidSucceed() {
|
|
||||||
showAlert("The app was successfully unlocked.", title: "Unlocked")
|
|
||||||
isUnlocked = true
|
|
||||||
|
|
||||||
showAllCapsAndScrollToTop()
|
|
||||||
updateNavigationItemTitleView()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadClassifier() -> Classifier? {
|
|
||||||
guard let model = app.database.storage.recognitionModel else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return Classifier(model: model)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func askUserForText(_ title: String, detail: String, existingText: String? = nil, placeholder: String? = "Cap name", yesText: String, noText: String = "Cancel", confirmed: @escaping (_ text: String) -> Void) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
let alertController = UIAlertController(
|
|
||||||
title: title,
|
|
||||||
message: detail,
|
|
||||||
preferredStyle: .alert)
|
|
||||||
|
|
||||||
alertController.addTextField { textField in
|
|
||||||
textField.placeholder = placeholder
|
|
||||||
textField.keyboardType = .default
|
|
||||||
textField.text = existingText
|
|
||||||
}
|
|
||||||
|
|
||||||
let action = UIAlertAction(title: yesText, style: .default) { _ in
|
|
||||||
guard let name = alertController.textFields?.first?.text else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
confirmed(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
let cancel = UIAlertAction(title: noText, style: .cancel)
|
|
||||||
|
|
||||||
alertController.addAction(action)
|
|
||||||
alertController.addAction(cancel)
|
|
||||||
self.present(alertController, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func presentUserBinaryChoice(_ title: String, detail: String, yesText: String, noText: String = "Cancel", dismissed: (() -> Void)? = nil, confirmed: @escaping () -> Void) {
|
|
||||||
let alert = UIAlertController(title: title, message: detail, preferredStyle: .alert)
|
|
||||||
|
|
||||||
let confirm = UIAlertAction(title: yesText, style: .default) { _ in
|
|
||||||
confirmed()
|
|
||||||
}
|
|
||||||
let cancel = UIAlertAction(title: noText, style: .cancel) { _ in
|
|
||||||
dismissed?()
|
|
||||||
}
|
|
||||||
alert.addAction(confirm)
|
|
||||||
alert.addAction(cancel)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.present(alert, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Classification
|
|
||||||
|
|
||||||
/// The similarity of the cap to the currently processed image
|
|
||||||
private func match(for cap: Int) -> Float? {
|
|
||||||
matches?[cap]
|
|
||||||
}
|
|
||||||
|
|
||||||
private func clearClassifierMatches() {
|
|
||||||
matches = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func classify(image: UIImage) {
|
|
||||||
guard let classifier = self.classifier else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
|
||||||
self.log("Classification starting...")
|
|
||||||
classifier.recognize(image: image) { matches in
|
|
||||||
guard let matches = matches else {
|
|
||||||
self.log("Failed to classify image")
|
|
||||||
self.matches = nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.log("Classification finished")
|
|
||||||
self.matches = matches
|
|
||||||
self.sortType = .match
|
|
||||||
self.sortAscending = false
|
|
||||||
self.showAllCapsAndScrollToTop()
|
|
||||||
DispatchQueue.global(qos: .background).async {
|
|
||||||
app.database.update(recognizedCaps: Set(matches.keys))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func classifyDummyImage() {
|
|
||||||
guard let classifier = self.classifier else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
|
||||||
classifier.recognize(image: UIImage(named: "launch")!) { matches in
|
|
||||||
guard let matches = matches else {
|
|
||||||
self.log("Failed to classify dummy image")
|
|
||||||
self.matches = nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.log("Dummy classification finished")
|
|
||||||
DispatchQueue.global(qos: .background).async {
|
|
||||||
app.database.update(recognizedCaps: Set(matches.keys))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Finishing downloads
|
|
||||||
|
|
||||||
private func didDownloadClassifier() {
|
|
||||||
guard let model = app.database.storage.recognitionModel else {
|
|
||||||
classifier = nil
|
|
||||||
return
|
|
||||||
}
|
|
||||||
classifier = Classifier(model: model)
|
|
||||||
guard let image = accessory!.currentImage else {
|
|
||||||
classifyDummyImage()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
classify(image: image)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Showing caps
|
|
||||||
|
|
||||||
private func reloadCapsFromDatabase() {
|
|
||||||
caps = app.database?.caps ?? []
|
|
||||||
showCaps()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
Match all cap names against the given string and return matches.
|
|
||||||
- note: Each space-separated part of the string is matched individually
|
|
||||||
*/
|
|
||||||
private func showCaps(matching text: String? = nil) {
|
|
||||||
DispatchQueue.global(qos: .userInteractive).async {
|
|
||||||
self.searchText = text
|
|
||||||
guard let t = text else {
|
|
||||||
self.show(caps: self.caps)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let found = self.filter(caps: self.caps, matching: t)
|
|
||||||
self.show(caps: found)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func show(caps: [Cap]) {
|
|
||||||
show(sortedCaps: sorted(caps: caps))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func show(sortedCaps caps: [Cap]) {
|
|
||||||
shownCaps = caps
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.tableView.reloadData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func filter(caps: [Cap], matching text: String) -> [Cap] {
|
|
||||||
let textParts = text.components(separatedBy: " ").filter { $0 != "" }
|
|
||||||
return caps.compactMap { cap -> Cap? in
|
|
||||||
// For each part of text, check if name contains it
|
|
||||||
for textItem in textParts {
|
|
||||||
if !cap.cleanName.contains(textItem) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func sorted(caps: [Cap]) -> [Cap] {
|
|
||||||
if sortAscending {
|
|
||||||
switch sortType {
|
|
||||||
case .id: return caps.sorted { $0.id < $1.id }
|
|
||||||
case .count: return caps.sorted { $0.count < $1.count }
|
|
||||||
case .name: return caps.sorted { $0.name < $1.name }
|
|
||||||
case .match: return caps.sorted { match(for: $0.id) ?? 0 < match(for: $1.id) ?? 0 }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch sortType {
|
|
||||||
case .id: return caps.sorted { $0.id > $1.id }
|
|
||||||
case .count: return caps.sorted { $0.count > $1.count }
|
|
||||||
case .name: return caps.sorted { $0.name > $1.name }
|
|
||||||
case .match: return caps.sorted { match(for: $0.id) ?? 0 > match(for: $1.id) ?? 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resets the cap list to its original state, discarding any previous sorting.
|
|
||||||
private func showAllCapsByDescendingId() {
|
|
||||||
sortType = .id
|
|
||||||
sortAscending = false
|
|
||||||
showAllCapsAndScrollToTop()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Display all caps in the table, and scrolls to the top
|
|
||||||
private func showAllCapsAndScrollToTop() {
|
|
||||||
showCaps()
|
|
||||||
tableViewScrollToTop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - TableView
|
|
||||||
|
|
||||||
/**
|
|
||||||
Scroll the table view to the top
|
|
||||||
*/
|
|
||||||
private func tableViewScrollToTop() {
|
|
||||||
guard shownCaps.count > 0 else { return }
|
|
||||||
let path = IndexPath(row: 0, section: 0)
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.tableView.scrollToRow(at: path, at: .top, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - SortControllerDelegate
|
|
||||||
|
|
||||||
extension TableView: SortControllerDelegate {
|
|
||||||
|
|
||||||
func sortController(didSelect sortType: SortCriteria, ascending: Bool) {
|
|
||||||
self.sortType = sortType
|
|
||||||
self.sortAscending = ascending
|
|
||||||
if sortType != .match {
|
|
||||||
clearClassifierMatches()
|
|
||||||
}
|
|
||||||
showAllCapsAndScrollToTop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - CameraControllerDelegate
|
|
||||||
|
|
||||||
extension TableView: CameraControllerDelegate {
|
|
||||||
|
|
||||||
func didCapture(image: UIImage) {
|
|
||||||
guard let cap = capToAddImageTo else {
|
|
||||||
accessory!.showImageView(with: image)
|
|
||||||
classify(image: image)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard app.database.add(image: image, for: cap) else {
|
|
||||||
self.error("Could not save image")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log("Added image for cap \(cap)")
|
|
||||||
self.capToAddImageTo = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func didCancel() {
|
|
||||||
capToAddImageTo = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UITableViewDataSource
|
|
||||||
|
|
||||||
extension TableView {
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: "cap") as! CapCell
|
|
||||||
let cap = shownCaps[indexPath.row]
|
|
||||||
|
|
||||||
configure(cell: cell, for: cap)
|
|
||||||
return cell
|
|
||||||
}
|
|
||||||
|
|
||||||
private func configure(cell: CapCell, for cap: Cap) {
|
|
||||||
let matchText = cap.matchLabelText(match: match(for: cap.id), appIsUnlocked: self.isUnlocked)
|
|
||||||
let countText = cap.countLabelText(appIsUnlocked: self.isUnlocked)
|
|
||||||
|
|
||||||
cell.id = cap.id
|
|
||||||
cell.set(name: cap.name)
|
|
||||||
cell.set(matchLabel: matchText)
|
|
||||||
cell.set(countLabel: countText)
|
|
||||||
|
|
||||||
if let image = imageProvider.image(for: cap.id) {
|
|
||||||
cell.set(image: image)
|
|
||||||
} else {
|
|
||||||
cell.set(image: nil)
|
|
||||||
app.database.downloadImage(for: cap.id) { _ in
|
|
||||||
// Delegate call will update image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
||||||
return shownCaps.count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UITableViewDelegate
|
|
||||||
|
|
||||||
extension TableView {
|
|
||||||
|
|
||||||
private func takeImage(for cap: Int) {
|
|
||||||
self.capToAddImageTo = cap
|
|
||||||
showCameraView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
|
||||||
// Prevent unauthorized users from selecting caps
|
|
||||||
isUnlocked ? indexPath : nil
|
|
||||||
}
|
|
||||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
||||||
defer {
|
|
||||||
tableView.deselectRow(at: indexPath, animated: true)
|
|
||||||
}
|
|
||||||
// Prevent unauthorized users from making changes
|
|
||||||
guard isUnlocked else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let cap = shownCaps[indexPath.row]
|
|
||||||
guard let image = accessory?.capImage.image else {
|
|
||||||
self.giveFeedback(.medium)
|
|
||||||
takeImage(for: cap.id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard app.database.add(image: image, for: cap.id) else {
|
|
||||||
self.giveFeedback(.heavy)
|
|
||||||
self.error("Could not save image")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.giveFeedback(.medium)
|
|
||||||
// Delegate call will update cell
|
|
||||||
self.accessory?.discardImage()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func giveFeedback(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
|
|
||||||
UIImpactFeedbackGenerator(style: style).impactOccurred()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
||||||
// Prevent unauthorized users from making changes
|
|
||||||
guard isUnlocked else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
let cap = shownCaps[indexPath.row]
|
|
||||||
|
|
||||||
let rename = UIContextualAction(style: .normal, title: "Rename\ncap") { (_, _, success) in
|
|
||||||
success(true)
|
|
||||||
self.rename(cap: cap, at: indexPath)
|
|
||||||
self.giveFeedback(.medium)
|
|
||||||
}
|
|
||||||
rename.backgroundColor = .blue
|
|
||||||
|
|
||||||
let image = UIContextualAction(style: .normal, title: "Change\nimage") { (_, _, success) in
|
|
||||||
self.giveFeedback(.medium)
|
|
||||||
let storyboard = UIStoryboard(name: "Main", bundle: nil)
|
|
||||||
let controller = storyboard.instantiateViewController(withIdentifier: "ImageSelector") as! ImageSelector
|
|
||||||
controller.cap = cap
|
|
||||||
controller.imageProvider = self.imageProvider
|
|
||||||
self.navigationController?.pushViewController(controller, animated: true)
|
|
||||||
success(true)
|
|
||||||
}
|
|
||||||
image.backgroundColor = .red
|
|
||||||
|
|
||||||
return UISwipeActionsConfiguration(actions: [rename, image])
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
|
||||||
let cap = shownCaps[indexPath.row]
|
|
||||||
|
|
||||||
var actions = [UIContextualAction]()
|
|
||||||
// Prevent unauthorized users from making changes
|
|
||||||
if isUnlocked {
|
|
||||||
let count = UIContextualAction(style: .normal, title: "Update\ncount") { (_, _, success) in
|
|
||||||
self.giveFeedback(.medium)
|
|
||||||
success(true)
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
|
||||||
app.database.downloadImageCount(for: cap.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
count.backgroundColor = .orange
|
|
||||||
actions.append(count)
|
|
||||||
}
|
|
||||||
|
|
||||||
let similar = UIContextualAction(style: .normal, title: "Similar\ncaps") { (_, _, success) in
|
|
||||||
self.giveFeedback(.medium)
|
|
||||||
self.accessory?.hideImageView()
|
|
||||||
guard let image = self.imageProvider.image(for: cap.id, version: 0) else {
|
|
||||||
success(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.classify(image: image)
|
|
||||||
success(true)
|
|
||||||
}
|
|
||||||
similar.backgroundColor = .blue
|
|
||||||
actions.append(similar)
|
|
||||||
|
|
||||||
return UISwipeActionsConfiguration(actions: actions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Logging
|
|
||||||
|
|
||||||
extension TableView: Logger { }
|
|
||||||
|
|
||||||
// MARK: - Protocol DatabaseDelegate
|
|
||||||
|
|
||||||
extension TableView: DatabaseDelegate {
|
|
||||||
|
|
||||||
func database(needsUserConfirmation title: String, body: String, shouldProceed: @escaping (Bool) -> Void) {
|
|
||||||
presentUserBinaryChoice(title, detail: body, yesText: "Download", noText: "Later", dismissed: {
|
|
||||||
shouldProceed(false)
|
|
||||||
}) {
|
|
||||||
shouldProceed(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func databaseHasNewClassifier() {
|
|
||||||
didDownloadClassifier()
|
|
||||||
}
|
|
||||||
|
|
||||||
func database(completedBackgroundWorkItem title: String, subtitle: String) {
|
|
||||||
set(title: title, subtitle: subtitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
func database(didFailBackgroundWork title: String, subtitle: String) {
|
|
||||||
set(title: title, subtitle: subtitle)
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(10)) {
|
|
||||||
self.updateNavigationItemTitleView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func databaseDidFinishBackgroundWork() {
|
|
||||||
// set(title: "All tasks completed", subtitle: titleText)
|
|
||||||
self.updateNavigationItemTitleView()
|
|
||||||
// DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
|
|
||||||
// self.updateNavigationItemTitleView()
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
func database(didAddCap cap: Cap) {
|
|
||||||
caps.append(cap)
|
|
||||||
updateNavigationItemTitleView()
|
|
||||||
guard let text = searchText else {
|
|
||||||
// All caps are shown
|
|
||||||
let newList = sorted(caps: caps)
|
|
||||||
updateShownCaps(newList, insertedId: cap.id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard filter(caps: [cap], matching: text) != [] else {
|
|
||||||
// Cap is not shown, so don't reload
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let newList = sorted(caps: filter(caps: caps, matching: text))
|
|
||||||
updateShownCaps(newList, insertedId: cap.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func database(didChangeCap id: Int) {
|
|
||||||
updateNavigationItemTitleView()
|
|
||||||
guard let cap = app.database.cap(for: id) else {
|
|
||||||
log("Changed cap \(id) not found in database")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if let index = caps.firstIndex(where: { $0.id == id}) {
|
|
||||||
caps[index] = cap
|
|
||||||
} else {
|
|
||||||
log("Cap not found in full list")
|
|
||||||
}
|
|
||||||
if let index = shownCaps.firstIndex(where: { $0.id == id}) {
|
|
||||||
shownCaps[index] = cap
|
|
||||||
}
|
|
||||||
guard let cell = visibleCell(for: id) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
configure(cell: cell, for: cap)
|
|
||||||
}
|
|
||||||
|
|
||||||
func database(didLoadImageForCap cap: Int) {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
guard let cell = self.visibleCell(for: cap) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
guard let image = self.imageProvider.image(for: cap) else {
|
|
||||||
self.log("No image for cap \(cap), although it should be loaded")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cell.set(image: image)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func databaseNeedsFullRefresh() {
|
|
||||||
reloadCapsFromDatabase()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func visibleCell(for cap: Int) -> CapCell? {
|
|
||||||
tableView.visibleCells
|
|
||||||
.map { $0 as! CapCell }
|
|
||||||
.first { $0.id == cap }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Protocol CapSearchDelegate
|
|
||||||
|
|
||||||
extension TableView: CapAccessoryDelegate {
|
|
||||||
|
|
||||||
func capSearchWasDismissed() {
|
|
||||||
showAllCapsAndScrollToTop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func capSearch(didChange text: String) {
|
|
||||||
let cleaned = text.clean
|
|
||||||
guard cleaned != "" else {
|
|
||||||
self.showCaps(matching: nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.showCaps(matching: cleaned)
|
|
||||||
}
|
|
||||||
|
|
||||||
func capAccessoryDidDiscardImage() {
|
|
||||||
matches = nil
|
|
||||||
showAllCapsByDescendingId()
|
|
||||||
}
|
|
||||||
|
|
||||||
func capAccessory(shouldSave image: UIImage) {
|
|
||||||
guard isUnlocked else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
saveNewCap(for: image)
|
|
||||||
}
|
|
||||||
|
|
||||||
func capAccessoryCameraButtonPressed() {
|
|
||||||
showCameraView()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
// Copyright 2018, Ralf Ebert
|
|
||||||
// License https://opensource.org/licenses/MIT
|
|
||||||
// License https://creativecommons.org/publicdomain/zero/1.0/
|
|
||||||
// Source https://www.ralfebert.de/ios-examples/uikit/choicepopover/
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
/**
|
|
||||||
By default, when you use:
|
|
||||||
|
|
||||||
```
|
|
||||||
controller.modalPresentationStyle = .popover
|
|
||||||
```
|
|
||||||
|
|
||||||
in a horizontally compact environment (iPhone in portrait mode), this option behaves the same as fullScreen.
|
|
||||||
You can make it to always show a popover by using:
|
|
||||||
|
|
||||||
```
|
|
||||||
let presentationController = AlwaysPresentAsPopover.configurePresentation(forController: controller)
|
|
||||||
```
|
|
||||||
*/
|
|
||||||
class AlwaysPresentAsPopover : NSObject, UIPopoverPresentationControllerDelegate {
|
|
||||||
|
|
||||||
// `sharedInstance` because the delegate property is weak - the delegate instance needs to be retained.
|
|
||||||
private static let sharedInstance = AlwaysPresentAsPopover()
|
|
||||||
|
|
||||||
private override init() {
|
|
||||||
super.init()
|
|
||||||
}
|
|
||||||
|
|
||||||
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
|
|
||||||
static func configurePresentation(forController controller : UIViewController) -> UIPopoverPresentationController {
|
|
||||||
controller.modalPresentationStyle = .popover
|
|
||||||
let presentationController = controller.presentationController as! UIPopoverPresentationController
|
|
||||||
presentationController.delegate = AlwaysPresentAsPopover.sharedInstance
|
|
||||||
return presentationController
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
//
|
|
||||||
// CropView.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 31.01.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
//@IBDesignable
|
|
||||||
class CropView: UIView {
|
|
||||||
|
|
||||||
@IBInspectable var lineColor: UIColor = UIColor.black
|
|
||||||
|
|
||||||
@IBInspectable var lineWidth: Float = 2
|
|
||||||
|
|
||||||
@IBInspectable var relativeSize: Float = 0.6 {
|
|
||||||
didSet { size = CGFloat(relativeSize) / 2 }
|
|
||||||
}
|
|
||||||
|
|
||||||
private var size: CGFloat = 0.3
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
|
||||||
super.init(frame: frame)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
|
||||||
super.init(coder: aDecoder)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func draw(_ rect: CGRect) {
|
|
||||||
let height = rect.height
|
|
||||||
let width = rect.width
|
|
||||||
|
|
||||||
let length = height > width ? width : height
|
|
||||||
|
|
||||||
let center = CGPoint(x: width / 2, y: height / 2)
|
|
||||||
let path = UIBezierPath()
|
|
||||||
|
|
||||||
path.lineWidth = CGFloat(self.lineWidth)
|
|
||||||
lineColor.setStroke()
|
|
||||||
|
|
||||||
path.addArc(
|
|
||||||
withCenter: center,
|
|
||||||
radius: length * size,
|
|
||||||
startAngle: 0,
|
|
||||||
endAngle: .pi * 2,
|
|
||||||
clockwise: true)
|
|
||||||
|
|
||||||
path.stroke()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
//
|
|
||||||
// RoundedButton.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 01.02.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
@IBDesignable
|
|
||||||
class RoundedButton: UIButton {
|
|
||||||
|
|
||||||
@IBInspectable var borderColor: UIColor = UIColor.black {
|
|
||||||
didSet {
|
|
||||||
layer.borderColor = borderColor.cgColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBInspectable var borderWidth: CGFloat = 0 {
|
|
||||||
didSet {
|
|
||||||
layer.borderWidth = borderWidth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//Normal state bg and border
|
|
||||||
@IBInspectable var normalBorderColor: UIColor? {
|
|
||||||
didSet {
|
|
||||||
layer.borderColor = normalBorderColor?.cgColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBInspectable var normalBackgroundColor: UIColor? {
|
|
||||||
didSet {
|
|
||||||
setBgColorForState(color: normalBackgroundColor, forState: [])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//Highlighted state bg and border
|
|
||||||
@IBInspectable var highlightedBorderColor: UIColor?
|
|
||||||
|
|
||||||
@IBInspectable var highlightedBackgroundColor: UIColor? {
|
|
||||||
didSet {
|
|
||||||
setBgColorForState(color: highlightedBackgroundColor, forState: .highlighted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private func setBgColorForState(color: UIColor?, forState: UIControl.State){
|
|
||||||
self.backgroundColor = color
|
|
||||||
// if color != nil {
|
|
||||||
// self.backgroundColor = color!
|
|
||||||
// setBackgroundImage(UIImage.imageWithColor(color: color!), for: forState)
|
|
||||||
// } else {
|
|
||||||
// setBackgroundImage(nil, for: forState)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutSubviews() {
|
|
||||||
super.layoutSubviews()
|
|
||||||
|
|
||||||
layer.cornerRadius = layer.frame.height / 2
|
|
||||||
// layer.cornerRadius = min(layer.frame.height, layer.frame.width) / 2
|
|
||||||
clipsToBounds = true
|
|
||||||
|
|
||||||
if borderWidth > 0 {
|
|
||||||
if state == [] && layer.borderColor == normalBorderColor?.cgColor {
|
|
||||||
layer.borderColor = normalBorderColor?.cgColor
|
|
||||||
} else if state == .highlighted && highlightedBorderColor != nil {
|
|
||||||
layer.borderColor = highlightedBorderColor!.cgColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension RoundedButton {
|
|
||||||
|
|
||||||
func set(template: String, with tint: UIColor) {
|
|
||||||
self.setImage(UIImage.templateImage(named: template), for: .normal)
|
|
||||||
self.tintColor = tint
|
|
||||||
self.borderColor = tint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//Extension Required by RoundedButton to create UIImage from UIColor
|
|
||||||
extension UIImage {
|
|
||||||
class func imageWithColor(color: UIColor) -> UIImage {
|
|
||||||
let rect: CGRect = CGRect(x: 0, y: 0, width: 1, height: 1)
|
|
||||||
UIGraphicsBeginImageContextWithOptions(CGSize(width: 1, height: 1), false, 1.0)
|
|
||||||
color.setFill()
|
|
||||||
UIRectFill(rect)
|
|
||||||
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
|
|
||||||
UIGraphicsEndImageContext()
|
|
||||||
return image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
|||||||
//
|
|
||||||
// RoundedImageView.swift
|
|
||||||
// CapFinder
|
|
||||||
//
|
|
||||||
// Created by User on 22.04.18.
|
|
||||||
// Copyright © 2018 User. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class RoundedImageView: UIImageView {
|
|
||||||
|
|
||||||
@IBInspectable var borderColor: UIColor = UIColor.black {
|
|
||||||
didSet {
|
|
||||||
layer.borderColor = borderColor.cgColor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBInspectable var borderWidth: CGFloat = 0 {
|
|
||||||
didSet {
|
|
||||||
layer.borderWidth = borderWidth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func layoutSubviews() {
|
|
||||||
super.layoutSubviews()
|
|
||||||
|
|
||||||
layer.cornerRadius = layer.frame.height / 2
|
|
||||||
clipsToBounds = true
|
|
||||||
}
|
|
||||||
}
|
|
31
Caps/Views/CapNameEntryView.swift
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
struct CapNameEntryView: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var name: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
TextField("Name", text: $name, prompt: Text("Enter name..."))
|
||||||
|
.padding(7)
|
||||||
|
.padding(.horizontal, 25)
|
||||||
|
.background(Color(.systemGray5))
|
||||||
|
.cornerRadius(8)
|
||||||
|
.overlay(
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: .squareAndPencil)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.leading, 8)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CapNameEntryView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
CapNameEntryView(name: .constant(""))
|
||||||
|
.previewLayout(.fixed(width: 375, height: 50))
|
||||||
|
}
|
||||||
|
}
|
77
Caps/Views/CapRowView.swift
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import CachedAsyncImage
|
||||||
|
|
||||||
|
struct CapRowView: View {
|
||||||
|
|
||||||
|
private let imageSize: CGFloat = 70
|
||||||
|
|
||||||
|
private let sufficientImageCount = 10
|
||||||
|
|
||||||
|
let cap: Cap
|
||||||
|
|
||||||
|
let match: Float?
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
var database: Database
|
||||||
|
|
||||||
|
var imageUrl: URL {
|
||||||
|
database.serverUrl.appendingPathComponent(cap.mainImagePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageCountText: String {
|
||||||
|
guard cap.imageCount != 1 else {
|
||||||
|
return "\(cap.id) (1 image)"
|
||||||
|
}
|
||||||
|
return "\(cap.id) (\(cap.imageCount) images)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Text(imageCountText)
|
||||||
|
.font(.footnote)
|
||||||
|
if !cap.classifiable(by: database.classifierVersion) {
|
||||||
|
Text("📵")
|
||||||
|
}
|
||||||
|
if cap.imageCount < sufficientImageCount {
|
||||||
|
Text("⚠️")
|
||||||
|
}
|
||||||
|
if database.hasPendingUpdates(for: cap.id) {
|
||||||
|
Text("⇅")
|
||||||
|
}
|
||||||
|
if database.hasPendingOperations(for: cap.id) {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 0)
|
||||||
|
.font(.footnote)
|
||||||
|
Text(cap.name)
|
||||||
|
.font(.headline)
|
||||||
|
.padding(.bottom, 3)
|
||||||
|
if let match = match {
|
||||||
|
Text("\(Int((match * 100).rounded())) % match")
|
||||||
|
.font(.footnote)
|
||||||
|
}
|
||||||
|
}//.padding(.vertical)
|
||||||
|
Spacer()
|
||||||
|
CachedAsyncImage(url: imageUrl, urlCache: database.imageCache) { image in
|
||||||
|
image.resizable()
|
||||||
|
} placeholder: {
|
||||||
|
ProgressView()
|
||||||
|
}
|
||||||
|
.frame(width: imageSize, height: imageSize)
|
||||||
|
.cornerRadius(imageSize / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CapRowView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
CapRowView(cap: Cap(id: 123, name: "My new cap"),
|
||||||
|
match: 0.13)
|
||||||
|
.previewLayout(.fixed(width: 375, height: 80))
|
||||||
|
.environmentObject(Database.mock)
|
||||||
|
}
|
||||||
|
}
|
13
Caps/Views/GridView.swift
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct GridView: View {
|
||||||
|
var body: some View {
|
||||||
|
Text("Grid view")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GridView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
GridView()
|
||||||
|
}
|
||||||
|
}
|
40
Caps/Views/SearchField.swift
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SFSafeSymbols
|
||||||
|
|
||||||
|
struct SearchField: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var searchString: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
TextField("Search", text: $searchString, prompt: Text("Search..."))
|
||||||
|
.padding(7)
|
||||||
|
.padding(.horizontal, 25)
|
||||||
|
.background(Color(.systemGray5))
|
||||||
|
.cornerRadius(8)
|
||||||
|
.overlay(
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: .magnifyingglass)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.leading, 8)
|
||||||
|
if searchString != "" {
|
||||||
|
Button(action: {
|
||||||
|
self.searchString = ""
|
||||||
|
}) {
|
||||||
|
Image(systemSymbol: .multiplyCircleFill)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.padding(.trailing, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SearchField_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SearchField(searchString: .constant(""))
|
||||||
|
.previewLayout(.fixed(width: 375, height: 50))
|
||||||
|
}
|
||||||
|
}
|
30
Caps/Views/SettingsStatisticRow.swift
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//
|
||||||
|
// SettingsStatisticRow.swift
|
||||||
|
// Caps
|
||||||
|
//
|
||||||
|
// Created by CH on 26.05.22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsStatisticRow: View {
|
||||||
|
|
||||||
|
let label: String
|
||||||
|
|
||||||
|
let value: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
Text(label)
|
||||||
|
Spacer()
|
||||||
|
Text(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsStatisticRow_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsStatisticRow(label: "Label", value: "Value")
|
||||||
|
.previewLayout(.fixed(width: 375, height: 40))
|
||||||
|
}
|
||||||
|
}
|
74
Caps/Views/SettingsView.swift
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SettingsView: View {
|
||||||
|
|
||||||
|
@EnvironmentObject
|
||||||
|
var database: Database
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var isPresented: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
|
HStack {
|
||||||
|
Text("Settings")
|
||||||
|
.font(.title2)
|
||||||
|
.bold()
|
||||||
|
Spacer()
|
||||||
|
Button(action: hide) {
|
||||||
|
Image(systemSymbol: .xmarkCircleFill)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.font(.system(size: 26))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Text("Statistics")
|
||||||
|
.font(.footnote)
|
||||||
|
.textCase(.uppercase)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.top)
|
||||||
|
Group {
|
||||||
|
SettingsStatisticRow(label: "Caps", value: "\(database.numberOfCaps)")
|
||||||
|
SettingsStatisticRow(label: "Total images", value: "\(database.numberOfImages)")
|
||||||
|
SettingsStatisticRow(label: "Images per cap", value: String(format: "%.1f", database.averageImageCount))
|
||||||
|
}.padding(.horizontal)
|
||||||
|
Text("Classifier")
|
||||||
|
.font(.footnote)
|
||||||
|
.textCase(.uppercase)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.top)
|
||||||
|
Group {
|
||||||
|
SettingsStatisticRow(label: "Version", value: "\(database.classifierVersion)")
|
||||||
|
SettingsStatisticRow(label: "Recognized caps", value: "\(database.classifierClassCount)")
|
||||||
|
}.padding(.horizontal)
|
||||||
|
Text("Storage")
|
||||||
|
.font(.footnote)
|
||||||
|
.textCase(.uppercase)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.padding(.top)
|
||||||
|
Group {
|
||||||
|
SettingsStatisticRow(label: "Image cache", value: byteString(database.imageCacheSize))
|
||||||
|
SettingsStatisticRow(label: "Database", value: byteString(database.databaseSize))
|
||||||
|
SettingsStatisticRow(label: "Classifier", value: byteString(database.classifierSize))
|
||||||
|
}.padding(.horizontal)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hide() {
|
||||||
|
isPresented = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func byteString(_ count: Int) -> String {
|
||||||
|
ByteCountFormatter.string(fromByteCount: Int64(count), countStyle: .file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SettingsView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SettingsView(isPresented: .constant(true))
|
||||||
|
.environmentObject(Database.mock)
|
||||||
|
.previewLayout(.fixed(width: 375, height: 330))
|
||||||
|
}
|
||||||
|
}
|
33
Caps/Views/SortCaseRowView.swift
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SortCaseRowView: View {
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var selectedType: SortCriteria
|
||||||
|
|
||||||
|
let type: SortCriteria
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button(action: { selectedType = type}) {
|
||||||
|
HStack {
|
||||||
|
Text(type.text)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
Spacer()
|
||||||
|
if selectedType == type {
|
||||||
|
Image(systemSymbol: .checkmark)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.background(Color(UIColor.systemGroupedBackground))
|
||||||
|
.cornerRadius(8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SortCaseRowView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SortCaseRowView(selectedType: .constant(.id), type: .id)
|
||||||
|
.previewLayout(.fixed(width: 375, height: 50))
|
||||||
|
}
|
||||||
|
}
|
95
Caps/Views/SortSelectionView.swift
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
private extension Binding where Value == SortCriteria {
|
||||||
|
|
||||||
|
func value() -> Binding<Int> {
|
||||||
|
return Binding<Int>(get:{ self.wrappedValue.rawValue },
|
||||||
|
set: { self.wrappedValue = .init(rawValue: $0)!})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct SortSelectionView: View {
|
||||||
|
|
||||||
|
let hasMatches: Bool
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var isPresented: Bool
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var sortType: SortCriteria
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var sortAscending: Bool
|
||||||
|
|
||||||
|
@Binding
|
||||||
|
var showGridView: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
|
HStack {
|
||||||
|
Text("List Settings").font(.title2).bold()
|
||||||
|
Spacer()
|
||||||
|
Button(action: { isPresented = false }) {
|
||||||
|
Image(systemSymbol: .xmarkCircleFill)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
.font(.system(size: 26))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.bottom)
|
||||||
|
Text("Sort by")
|
||||||
|
.font(.footnote)
|
||||||
|
.textCase(.uppercase)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Picker("Sort type", selection: $sortType.value()) {
|
||||||
|
Text(SortCriteria.id.text).tag(SortCriteria.id.rawValue)
|
||||||
|
Text(SortCriteria.name.text).tag(SortCriteria.name.rawValue)
|
||||||
|
Text(SortCriteria.count.text).tag(SortCriteria.count.rawValue)
|
||||||
|
if hasMatches {
|
||||||
|
Text(SortCriteria.match.text).tag(SortCriteria.match.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pickerStyle(SegmentedPickerStyle())
|
||||||
|
.padding(.bottom)
|
||||||
|
Text("Sort order")
|
||||||
|
.font(.footnote)
|
||||||
|
.textCase(.uppercase)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Picker("Sort order", selection: $sortAscending) {
|
||||||
|
Text("Ascending").tag(true)
|
||||||
|
Text("Descending").tag(false)
|
||||||
|
}
|
||||||
|
.pickerStyle(SegmentedPickerStyle())
|
||||||
|
.padding(.bottom)
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button(action: showGrid) {
|
||||||
|
HStack {
|
||||||
|
Image(systemSymbol: .circleHexagongrid)
|
||||||
|
Text("Show grid")
|
||||||
|
}
|
||||||
|
}.padding()
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showGrid() {
|
||||||
|
showGridView = true
|
||||||
|
isPresented = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SortSelectionView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
SortSelectionView(
|
||||||
|
hasMatches: true,
|
||||||
|
isPresented: .constant(true),
|
||||||
|
sortType: .constant(.id),
|
||||||
|
sortAscending: .constant(false),
|
||||||
|
showGridView: .constant(false))
|
||||||
|
.previewLayout(.fixed(width: 375, height: 250))
|
||||||
|
}
|
||||||
|
}
|