Start version 2
@ -3,227 +3,196 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 52;
|
||||
objectVersion = 55;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
5904C33A2199C9FA0046A573 /* SortController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5904C3392199C9FA0046A573 /* SortController.swift */; };
|
||||
5904C33C2199D0260046A573 /* AlwaysShowPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */; };
|
||||
59158B1621E37B0200D90CB0 /* GridViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59158B1521E37B0200D90CB0 /* GridViewController.swift */; };
|
||||
59158B1821E4C9AC00D90CB0 /* NavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59158B1721E4C9AC00D90CB0 /* NavigationController.swift */; };
|
||||
591832CE21A2A97E00E5987D /* Cap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591832CD21A2A97E00E5987D /* Cap.swift */; };
|
||||
591FDD1E234E151600AA379E /* SearchAndDisplayAccessory.xib in Resources */ = {isa = PBXBuildFile; fileRef = 591FDD1D234E151600AA379E /* SearchAndDisplayAccessory.xib */; };
|
||||
591FDD20234E162000AA379E /* SearchAndDisplayAccessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 591FDD1F234E162000AA379E /* SearchAndDisplayAccessory.swift */; };
|
||||
88A89ECE25AF420F00323B64 /* DispatchGroup+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A89ECD25AF420F00323B64 /* DispatchGroup+Extensions.swift */; };
|
||||
CE0A501124752A9800A9E753 /* TileImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0A501024752A9800A9E753 /* TileImage.swift */; };
|
||||
CE0A5013247D745200A9E753 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0A5012247D745200A9E753 /* Colors.swift */; };
|
||||
CE56CECE209D81DE00932C01 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CECD209D81DE00932C01 /* AppDelegate.swift */; };
|
||||
CE56CED3209D81DE00932C01 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE56CED1209D81DE00932C01 /* Main.storyboard */; };
|
||||
CE56CED5209D81E000932C01 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE56CED4209D81E000932C01 /* Assets.xcassets */; };
|
||||
CE56CED8209D81E000932C01 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE56CED6209D81E000932C01 /* LaunchScreen.storyboard */; };
|
||||
CE56CEF8209D83B800932C01 /* CapCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE0209D83B200932C01 /* CapCell.swift */; };
|
||||
CE56CEFE209D83B800932C01 /* RoundedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE6209D83B300932C01 /* RoundedButton.swift */; };
|
||||
CE56CEFF209D83B800932C01 /* CameraController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEE7209D83B300932C01 /* CameraController.swift */; };
|
||||
CE56CF02209D83B800932C01 /* RoundedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEA209D83B400932C01 /* RoundedImageView.swift */; };
|
||||
CE56CF03209D83B800932C01 /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEB209D83B400932C01 /* TableView.swift */; };
|
||||
CE56CF04209D83B800932C01 /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEC209D83B400932C01 /* UIViewExtensions.swift */; };
|
||||
CE56CF05209D83B800932C01 /* ViewControllerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEED209D83B400932C01 /* ViewControllerExtensions.swift */; };
|
||||
CE56CF06209D83B800932C01 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEE209D83B500932C01 /* CameraView.swift */; };
|
||||
CE56CF07209D83B800932C01 /* ImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEEF209D83B500932C01 /* ImageCell.swift */; };
|
||||
CE56CF08209D83B800932C01 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF0209D83B500932C01 /* Storage.swift */; };
|
||||
CE56CF09209D83B800932C01 /* Classifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF1209D83B500932C01 /* Classifier.swift */; };
|
||||
CE56CF0A209D83B800932C01 /* CropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF2209D83B600932C01 /* CropView.swift */; };
|
||||
CE56CF0B209D83B800932C01 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF3209D83B600932C01 /* Logger.swift */; };
|
||||
CE56CF0D209D83B800932C01 /* ImageSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF5209D83B600932C01 /* ImageSelector.swift */; };
|
||||
CE56CF0E209D83B800932C01 /* PhotoCaptureHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF6209D83B700932C01 /* PhotoCaptureHandler.swift */; };
|
||||
CE56CF0F209D83B800932C01 /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE56CEF7209D83B700932C01 /* UIImage+Extensions.swift */; };
|
||||
CE5B7CFC24562673002E5C06 /* Download.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5B7CFB24562673002E5C06 /* Download.swift */; };
|
||||
CE5B7CFE245626D3002E5C06 /* Upload.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5B7CFD245626D3002E5C06 /* Upload.swift */; };
|
||||
CE5B7D032458C921002E5C06 /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = CE5B7D022458C921002E5C06 /* Reachability */; };
|
||||
CE6E4828246C304100570CB0 /* CGImagePropertyOrientation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6E4827246C304100570CB0 /* CGImagePropertyOrientation+Extensions.swift */; };
|
||||
CE85AA16246A96C3002D1074 /* UINavigationItem+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE85AA15246A96C3002D1074 /* UINavigationItem+Extensions.swift */; };
|
||||
CE85AA18246B012B002D1074 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE85AA17246B012B002D1074 /* Array+Extensions.swift */; };
|
||||
CEB269572445DB56004B74B3 /* SQLite in Frameworks */ = {isa = PBXBuildFile; productRef = CEB269562445DB56004B74B3 /* SQLite */; };
|
||||
CEB269592445DB72004B74B3 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB269582445DB72004B74B3 /* Database.swift */; };
|
||||
CEB2695B2445E54E004B74B3 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB2695A2445E54E004B74B3 /* UIColor+Extensions.swift */; };
|
||||
E25AAC7C283D855D006E9E7F /* CapsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC7B283D855D006E9E7F /* CapsApp.swift */; };
|
||||
E25AAC7E283D855D006E9E7F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC7D283D855D006E9E7F /* ContentView.swift */; };
|
||||
E25AAC80283D855F006E9E7F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E25AAC7F283D855F006E9E7F /* Assets.xcassets */; };
|
||||
E25AAC83283D855F006E9E7F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E25AAC82283D855F006E9E7F /* Preview Assets.xcassets */; };
|
||||
E25AAC8B283D868D006E9E7F /* Classifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC8A283D868D006E9E7F /* Classifier.swift */; };
|
||||
E25AAC8D283D86CF006E9E7F /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC8C283D86CF006E9E7F /* Logger.swift */; };
|
||||
E25AAC90283D871E006E9E7F /* CGImagePropertyOrientation+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC8F283D871E006E9E7F /* CGImagePropertyOrientation+Extensions.swift */; };
|
||||
E25AAC92283D8808006E9E7F /* CapData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC91283D8808006E9E7F /* CapData.swift */; };
|
||||
E25AAC94283D88A4006E9E7F /* Cap.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC93283D88A4006E9E7F /* Cap.swift */; };
|
||||
E25AAC96283E14DF006E9E7F /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC95283E14DF006E9E7F /* Database.swift */; };
|
||||
E25AAC9B283E3395006E9E7F /* CapRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25AAC9A283E3395006E9E7F /* CapRowView.swift */; };
|
||||
E27E15E1283E418600F6804A /* CachedAsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = E27E15E0283E418600F6804A /* CachedAsyncImage */; };
|
||||
E2EA00C3283E672A00F7B269 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = E2EA00C2283E672A00F7B269 /* SFSafeSymbols */; };
|
||||
E2EA00C5283EA72000F7B269 /* SortCriteria.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00C4283EA72000F7B269 /* SortCriteria.swift */; };
|
||||
E2EA00C7283EAA0100F7B269 /* SortSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */; };
|
||||
E2EA00CA283EACB200F7B269 /* BottomSheet in Frameworks */ = {isa = PBXBuildFile; productRef = E2EA00C9283EACB200F7B269 /* BottomSheet */; };
|
||||
E2EA00CC283EB43E00F7B269 /* SortCaseRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */; };
|
||||
E2EA00CE283EBEB600F7B269 /* SearchField.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00CD283EBEB600F7B269 /* SearchField.swift */; };
|
||||
E2EA00D1283EDD6300F7B269 /* CameraManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00D0283EDD6300F7B269 /* CameraManager.swift */; };
|
||||
E2EA00D3283EDDF700F7B269 /* CameraError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00D2283EDDF700F7B269 /* CameraError.swift */; };
|
||||
E2EA00D5283EDFA200F7B269 /* FrameManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00D4283EDFA200F7B269 /* FrameManager.swift */; };
|
||||
E2EA00D9283F5BB900F7B269 /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00D8283F5BB900F7B269 /* CameraView.swift */; };
|
||||
E2EA00DB283F5C0600F7B269 /* ContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00DA283F5C0600F7B269 /* ContentViewModel.swift */; };
|
||||
E2EA00DD283F5C6A00F7B269 /* FrameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00DC283F5C6A00F7B269 /* FrameView.swift */; };
|
||||
E2EA00DF283F5CA000F7B269 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00DE283F5CA000F7B269 /* ErrorView.swift */; };
|
||||
E2EA00E1283F658E00F7B269 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00E0283F658E00F7B269 /* SettingsView.swift */; };
|
||||
E2EA00E3283F662800F7B269 /* GridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00E2283F662800F7B269 /* GridView.swift */; };
|
||||
E2EA00E5283F69DF00F7B269 /* SettingsStatisticRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00E4283F69DF00F7B269 /* SettingsStatisticRow.swift */; };
|
||||
E2EA00E7283F6D0800F7B269 /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00E6283F6D0800F7B269 /* URL+Extensions.swift */; };
|
||||
E2EA00EB284109CC00F7B269 /* CGImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00EA284109CC00F7B269 /* CGImage+Extensions.swift */; };
|
||||
E2EA00ED2841170100F7B269 /* UIImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00EC2841170100F7B269 /* UIImage+Extensions.swift */; };
|
||||
E2EA00EF28420AA000F7B269 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00EE28420AA000F7B269 /* Data+Extensions.swift */; };
|
||||
E2EA00F328438E6B00F7B269 /* CapNameEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EA00F228438E6B00F7B269 /* CapNameEntryView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
5904C3392199C9FA0046A573 /* SortController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortController.swift; sourceTree = "<group>"; };
|
||||
5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlwaysShowPopup.swift; sourceTree = "<group>"; };
|
||||
59158B1521E37B0200D90CB0 /* GridViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridViewController.swift; sourceTree = "<group>"; };
|
||||
59158B1721E4C9AC00D90CB0 /* NavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationController.swift; sourceTree = "<group>"; };
|
||||
591832CD21A2A97E00E5987D /* Cap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cap.swift; sourceTree = "<group>"; };
|
||||
591FDD1D234E151600AA379E /* SearchAndDisplayAccessory.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SearchAndDisplayAccessory.xib; sourceTree = "<group>"; };
|
||||
591FDD1F234E162000AA379E /* SearchAndDisplayAccessory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchAndDisplayAccessory.swift; sourceTree = "<group>"; };
|
||||
88A89ECD25AF420F00323B64 /* DispatchGroup+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchGroup+Extensions.swift"; sourceTree = "<group>"; };
|
||||
CE0A501024752A9800A9E753 /* TileImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TileImage.swift; sourceTree = "<group>"; };
|
||||
CE0A5012247D745200A9E753 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
|
||||
CE56CECA209D81DD00932C01 /* Caps.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Caps.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CE56CECD209D81DE00932C01 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
CE56CED2209D81DE00932C01 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
CE56CED4209D81E000932C01 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
CE56CED7209D81E000932C01 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
CE56CED9209D81E000932C01 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
CE56CEE0209D83B200932C01 /* CapCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CapCell.swift; sourceTree = "<group>"; };
|
||||
CE56CEE6209D83B300932C01 /* RoundedButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedButton.swift; sourceTree = "<group>"; };
|
||||
CE56CEE7209D83B300932C01 /* CameraController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraController.swift; sourceTree = "<group>"; };
|
||||
CE56CEE8209D83B300932C01 /* UIAlertControllerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIAlertControllerExtensions.swift; sourceTree = "<group>"; };
|
||||
CE56CEEA209D83B400932C01 /* RoundedImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedImageView.swift; sourceTree = "<group>"; };
|
||||
CE56CEEB209D83B400932C01 /* TableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TableView.swift; path = ../TableView.swift; sourceTree = "<group>"; };
|
||||
CE56CEEC209D83B400932C01 /* UIViewExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewExtensions.swift; sourceTree = "<group>"; };
|
||||
CE56CEED209D83B400932C01 /* ViewControllerExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerExtensions.swift; sourceTree = "<group>"; };
|
||||
CE56CEEE209D83B500932C01 /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
|
||||
CE56CEEF209D83B500932C01 /* ImageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCell.swift; sourceTree = "<group>"; };
|
||||
CE56CEF0209D83B500932C01 /* Storage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
|
||||
CE56CEF1209D83B500932C01 /* Classifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Classifier.swift; sourceTree = "<group>"; };
|
||||
CE56CEF2209D83B600932C01 /* CropView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropView.swift; sourceTree = "<group>"; };
|
||||
CE56CEF3209D83B600932C01 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
CE56CEF5209D83B600932C01 /* ImageSelector.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageSelector.swift; sourceTree = "<group>"; };
|
||||
CE56CEF6209D83B700932C01 /* PhotoCaptureHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureHandler.swift; sourceTree = "<group>"; };
|
||||
CE56CEF7209D83B700932C01 /* 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>"; };
|
||||
E25AAC78283D855D006E9E7F /* Caps.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Caps.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
E25AAC7B283D855D006E9E7F /* CapsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapsApp.swift; sourceTree = "<group>"; };
|
||||
E25AAC7D283D855D006E9E7F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
E25AAC7F283D855F006E9E7F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
E25AAC82283D855F006E9E7F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
E25AAC8A283D868D006E9E7F /* Classifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Classifier.swift; sourceTree = "<group>"; };
|
||||
E25AAC8C283D86CF006E9E7F /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
E25AAC8F283D871E006E9E7F /* CGImagePropertyOrientation+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGImagePropertyOrientation+Extensions.swift"; sourceTree = "<group>"; };
|
||||
E25AAC91283D8808006E9E7F /* CapData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapData.swift; sourceTree = "<group>"; };
|
||||
E25AAC93283D88A4006E9E7F /* Cap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cap.swift; sourceTree = "<group>"; };
|
||||
E25AAC95283E14DF006E9E7F /* Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = "<group>"; };
|
||||
E25AAC9A283E3395006E9E7F /* CapRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapRowView.swift; sourceTree = "<group>"; };
|
||||
E2EA00C4283EA72000F7B269 /* SortCriteria.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortCriteria.swift; sourceTree = "<group>"; };
|
||||
E2EA00C6283EAA0100F7B269 /* SortSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortSelectionView.swift; sourceTree = "<group>"; };
|
||||
E2EA00CB283EB43E00F7B269 /* SortCaseRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortCaseRowView.swift; sourceTree = "<group>"; };
|
||||
E2EA00CD283EBEB600F7B269 /* SearchField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchField.swift; sourceTree = "<group>"; };
|
||||
E2EA00D0283EDD6300F7B269 /* CameraManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraManager.swift; sourceTree = "<group>"; };
|
||||
E2EA00D2283EDDF700F7B269 /* CameraError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraError.swift; sourceTree = "<group>"; };
|
||||
E2EA00D4283EDFA200F7B269 /* FrameManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameManager.swift; sourceTree = "<group>"; };
|
||||
E2EA00D8283F5BB900F7B269 /* CameraView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
|
||||
E2EA00DA283F5C0600F7B269 /* ContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentViewModel.swift; sourceTree = "<group>"; };
|
||||
E2EA00DC283F5C6A00F7B269 /* FrameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameView.swift; sourceTree = "<group>"; };
|
||||
E2EA00DE283F5CA000F7B269 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
|
||||
E2EA00E0283F658E00F7B269 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
E2EA00E2283F662800F7B269 /* GridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridView.swift; sourceTree = "<group>"; };
|
||||
E2EA00E4283F69DF00F7B269 /* SettingsStatisticRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStatisticRow.swift; sourceTree = "<group>"; };
|
||||
E2EA00E6283F6D0800F7B269 /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = "<group>"; };
|
||||
E2EA00EA284109CC00F7B269 /* CGImage+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGImage+Extensions.swift"; sourceTree = "<group>"; };
|
||||
E2EA00EC2841170100F7B269 /* UIImage+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Extensions.swift"; sourceTree = "<group>"; };
|
||||
E2EA00EE28420AA000F7B269 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
|
||||
E2EA00F228438E6B00F7B269 /* CapNameEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapNameEntryView.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
CE56CEC7209D81DD00932C01 /* Frameworks */ = {
|
||||
E25AAC75283D855D006E9E7F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CEB269572445DB56004B74B3 /* SQLite in Frameworks */,
|
||||
CE5B7D032458C921002E5C06 /* Reachability in Frameworks */,
|
||||
E2EA00C3283E672A00F7B269 /* SFSafeSymbols in Frameworks */,
|
||||
E2EA00CA283EACB200F7B269 /* BottomSheet in Frameworks */,
|
||||
E27E15E1283E418600F6804A /* CachedAsyncImage in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
CE56CEC1209D81DD00932C01 = {
|
||||
E25AAC6F283D855D006E9E7F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE56CECC209D81DD00932C01 /* Caps */,
|
||||
CE56CECB209D81DD00932C01 /* Products */,
|
||||
E25AAC7A283D855D006E9E7F /* Caps */,
|
||||
E25AAC79283D855D006E9E7F /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE56CECB209D81DD00932C01 /* Products */ = {
|
||||
E25AAC79283D855D006E9E7F /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE56CECA209D81DD00932C01 /* Caps.app */,
|
||||
E25AAC78283D855D006E9E7F /* Caps.app */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE56CECC209D81DD00932C01 /* Caps */ = {
|
||||
E25AAC7A283D855D006E9E7F /* Caps */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE56CECD209D81DE00932C01 /* AppDelegate.swift */,
|
||||
CE56CED1209D81DE00932C01 /* Main.storyboard */,
|
||||
CEF3874D209D9378001C8D3C /* Capture */,
|
||||
CEF38750209D93D1001C8D3C /* Data */,
|
||||
CEF3874B209D932E001C8D3C /* View Components */,
|
||||
CEF3874F209D93A6001C8D3C /* Presentation */,
|
||||
CEF3874C209D935E001C8D3C /* Extensions */,
|
||||
CE56CEF3209D83B600932C01 /* Logger.swift */,
|
||||
CE56CEDF209D81FD00932C01 /* Support */,
|
||||
E25AAC7B283D855D006E9E7F /* CapsApp.swift */,
|
||||
E25AAC7D283D855D006E9E7F /* ContentView.swift */,
|
||||
E2EA00CF283EDD2C00F7B269 /* Camera */,
|
||||
E25AAC97283E337C006E9E7F /* Views */,
|
||||
E25AAC89283D8666006E9E7F /* Data */,
|
||||
E25AAC7F283D855F006E9E7F /* Assets.xcassets */,
|
||||
E25AAC8E283D870F006E9E7F /* Extensions */,
|
||||
E25AAC8C283D86CF006E9E7F /* Logger.swift */,
|
||||
E25AAC81283D855F006E9E7F /* Preview Content */,
|
||||
);
|
||||
path = Caps;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CE56CEDF209D81FD00932C01 /* Support */ = {
|
||||
E25AAC81283D855F006E9E7F /* Preview Content */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE56CED4209D81E000932C01 /* Assets.xcassets */,
|
||||
CE56CED6209D81E000932C01 /* LaunchScreen.storyboard */,
|
||||
CE56CED9209D81E000932C01 /* Info.plist */,
|
||||
E25AAC82283D855F006E9E7F /* Preview Assets.xcassets */,
|
||||
);
|
||||
name = Support;
|
||||
path = "Preview Content";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEF3874B209D932E001C8D3C /* View Components */ = {
|
||||
E25AAC89283D8666006E9E7F /* Data */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5904C33B2199D0260046A573 /* AlwaysShowPopup.swift */,
|
||||
CE56CEF2209D83B600932C01 /* CropView.swift */,
|
||||
CE56CEEA209D83B400932C01 /* RoundedImageView.swift */,
|
||||
CE56CEE6209D83B300932C01 /* RoundedButton.swift */,
|
||||
E25AAC8A283D868D006E9E7F /* Classifier.swift */,
|
||||
E2EA00C4283EA72000F7B269 /* SortCriteria.swift */,
|
||||
E25AAC93283D88A4006E9E7F /* Cap.swift */,
|
||||
E25AAC91283D8808006E9E7F /* CapData.swift */,
|
||||
E25AAC95283E14DF006E9E7F /* Database.swift */,
|
||||
);
|
||||
path = "View Components";
|
||||
path = Data;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEF3874C209D935E001C8D3C /* Extensions */ = {
|
||||
E25AAC8E283D870F006E9E7F /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE56CEE8209D83B300932C01 /* UIAlertControllerExtensions.swift */,
|
||||
CE6E4827246C304100570CB0 /* CGImagePropertyOrientation+Extensions.swift */,
|
||||
CE85AA17246B012B002D1074 /* Array+Extensions.swift */,
|
||||
CE85AA15246A96C3002D1074 /* UINavigationItem+Extensions.swift */,
|
||||
CEB2695A2445E54E004B74B3 /* UIColor+Extensions.swift */,
|
||||
CE56CEF7209D83B700932C01 /* UIImage+Extensions.swift */,
|
||||
CE56CEEC209D83B400932C01 /* UIViewExtensions.swift */,
|
||||
CE56CEED209D83B400932C01 /* ViewControllerExtensions.swift */,
|
||||
88A89ECD25AF420F00323B64 /* DispatchGroup+Extensions.swift */,
|
||||
E25AAC8F283D871E006E9E7F /* CGImagePropertyOrientation+Extensions.swift */,
|
||||
E2EA00EC2841170100F7B269 /* UIImage+Extensions.swift */,
|
||||
E2EA00E6283F6D0800F7B269 /* URL+Extensions.swift */,
|
||||
E2EA00EA284109CC00F7B269 /* CGImage+Extensions.swift */,
|
||||
E2EA00EE28420AA000F7B269 /* Data+Extensions.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CEF3874D209D9378001C8D3C /* Capture */ = {
|
||||
E25AAC97283E337C006E9E7F /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE56CEE7209D83B300932C01 /* CameraController.swift */,
|
||||
CE56CEEE209D83B500932C01 /* CameraView.swift */,
|
||||
CE56CEF6209D83B700932C01 /* PhotoCaptureHandler.swift */,
|
||||
E25AAC9A283E3395006E9E7F /* CapRowView.swift */,
|
||||
E2EA00E2283F662800F7B269 /* GridView.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>";
|
||||
};
|
||||
CEF3874F209D93A6001C8D3C /* Presentation */ = {
|
||||
E2EA00CF283EDD2C00F7B269 /* Camera */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CE56CEEB209D83B400932C01 /* TableView.swift */,
|
||||
591FDD1D234E151600AA379E /* SearchAndDisplayAccessory.xib */,
|
||||
591FDD1F234E162000AA379E /* SearchAndDisplayAccessory.swift */,
|
||||
59158B1721E4C9AC00D90CB0 /* NavigationController.swift */,
|
||||
CE56CEE0209D83B200932C01 /* CapCell.swift */,
|
||||
CE56CEF5209D83B600932C01 /* ImageSelector.swift */,
|
||||
CE56CEEF209D83B500932C01 /* ImageCell.swift */,
|
||||
5904C3392199C9FA0046A573 /* SortController.swift */,
|
||||
59158B1521E37B0200D90CB0 /* GridViewController.swift */,
|
||||
E2EA00D0283EDD6300F7B269 /* CameraManager.swift */,
|
||||
E2EA00D8283F5BB900F7B269 /* CameraView.swift */,
|
||||
E2EA00DA283F5C0600F7B269 /* ContentViewModel.swift */,
|
||||
E2EA00DC283F5C6A00F7B269 /* FrameView.swift */,
|
||||
E2EA00D4283EDFA200F7B269 /* FrameManager.swift */,
|
||||
E2EA00D2283EDDF700F7B269 /* CameraError.swift */,
|
||||
E2EA00DE283F5CA000F7B269 /* ErrorView.swift */,
|
||||
);
|
||||
path = Presentation;
|
||||
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;
|
||||
path = Camera;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
CE56CEC9209D81DD00932C01 /* Caps */ = {
|
||||
E25AAC77283D855D006E9E7F /* Caps */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = CE56CEDC209D81E000932C01 /* Build configuration list for PBXNativeTarget "Caps" */;
|
||||
buildConfigurationList = E25AAC86283D855F006E9E7F /* Build configuration list for PBXNativeTarget "Caps" */;
|
||||
buildPhases = (
|
||||
CE56CEC6209D81DD00932C01 /* Sources */,
|
||||
CE56CEC7209D81DD00932C01 /* Frameworks */,
|
||||
CE56CEC8209D81DD00932C01 /* Resources */,
|
||||
E25AAC74283D855D006E9E7F /* Sources */,
|
||||
E25AAC75283D855D006E9E7F /* Frameworks */,
|
||||
E25AAC76283D855D006E9E7F /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@ -231,141 +200,110 @@
|
||||
);
|
||||
name = Caps;
|
||||
packageProductDependencies = (
|
||||
CEB269562445DB56004B74B3 /* SQLite */,
|
||||
CE5B7D022458C921002E5C06 /* Reachability */,
|
||||
E27E15E0283E418600F6804A /* CachedAsyncImage */,
|
||||
E2EA00C2283E672A00F7B269 /* SFSafeSymbols */,
|
||||
E2EA00C9283EACB200F7B269 /* BottomSheet */,
|
||||
);
|
||||
productName = CapCollector;
|
||||
productReference = CE56CECA209D81DD00932C01 /* Caps.app */;
|
||||
productName = Caps;
|
||||
productReference = E25AAC78283D855D006E9E7F /* Caps.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
CE56CEC2209D81DD00932C01 /* Project object */ = {
|
||||
E25AAC70283D855D006E9E7F /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0940;
|
||||
LastUpgradeCheck = 1200;
|
||||
ORGANIZATIONNAME = CH;
|
||||
BuildIndependentTargetsInParallel = 1;
|
||||
LastSwiftUpdateCheck = 1340;
|
||||
LastUpgradeCheck = 1340;
|
||||
TargetAttributes = {
|
||||
CE56CEC9209D81DD00932C01 = {
|
||||
CreatedOnToolsVersion = 9.4;
|
||||
LastSwiftMigration = 1100;
|
||||
SystemCapabilities = {
|
||||
com.apple.BackgroundModes = {
|
||||
enabled = 0;
|
||||
E25AAC77283D855D006E9E7F = {
|
||||
CreatedOnToolsVersion = 13.4;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = CE56CEC5209D81DD00932C01 /* Build configuration list for PBXProject "Caps" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
buildConfigurationList = E25AAC73283D855D006E9E7F /* Build configuration list for PBXProject "Caps" */;
|
||||
compatibilityVersion = "Xcode 13.0";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = CE56CEC1209D81DD00932C01;
|
||||
mainGroup = E25AAC6F283D855D006E9E7F;
|
||||
packageReferences = (
|
||||
CEB269552445DB56004B74B3 /* XCRemoteSwiftPackageReference "SQLite" */,
|
||||
CE5B7D012458C921002E5C06 /* XCRemoteSwiftPackageReference "Reachability" */,
|
||||
E27E15DF283E418600F6804A /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */,
|
||||
E2EA00C1283E672A00F7B269 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */,
|
||||
E2EA00C8283EACB200F7B269 /* XCRemoteSwiftPackageReference "bottom-sheet" */,
|
||||
);
|
||||
productRefGroup = CE56CECB209D81DD00932C01 /* Products */;
|
||||
productRefGroup = E25AAC79283D855D006E9E7F /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
CE56CEC9209D81DD00932C01 /* Caps */,
|
||||
E25AAC77283D855D006E9E7F /* Caps */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
CE56CEC8209D81DD00932C01 /* Resources */ = {
|
||||
E25AAC76283D855D006E9E7F /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE56CED8209D81E000932C01 /* LaunchScreen.storyboard in Resources */,
|
||||
591FDD1E234E151600AA379E /* SearchAndDisplayAccessory.xib in Resources */,
|
||||
CE56CED5209D81E000932C01 /* Assets.xcassets in Resources */,
|
||||
CE56CED3209D81DE00932C01 /* Main.storyboard in Resources */,
|
||||
E25AAC83283D855F006E9E7F /* Preview Assets.xcassets in Resources */,
|
||||
E25AAC80283D855F006E9E7F /* Assets.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
CE56CEC6209D81DD00932C01 /* Sources */ = {
|
||||
E25AAC74283D855D006E9E7F /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
CE56CF09209D83B800932C01 /* Classifier.swift in Sources */,
|
||||
CE0A5013247D745200A9E753 /* Colors.swift in Sources */,
|
||||
5904C33A2199C9FA0046A573 /* SortController.swift in Sources */,
|
||||
CE56CF0B209D83B800932C01 /* Logger.swift in Sources */,
|
||||
CE56CF04209D83B800932C01 /* UIViewExtensions.swift in Sources */,
|
||||
59158B1821E4C9AC00D90CB0 /* NavigationController.swift in Sources */,
|
||||
591832CE21A2A97E00E5987D /* Cap.swift in Sources */,
|
||||
CE56CF08209D83B800932C01 /* Storage.swift in Sources */,
|
||||
CE56CF0F209D83B800932C01 /* UIImage+Extensions.swift in Sources */,
|
||||
CE5B7CFC24562673002E5C06 /* Download.swift in Sources */,
|
||||
CE85AA18246B012B002D1074 /* Array+Extensions.swift in Sources */,
|
||||
CE56CF03209D83B800932C01 /* TableView.swift in Sources */,
|
||||
CEB2695B2445E54E004B74B3 /* UIColor+Extensions.swift in Sources */,
|
||||
591FDD20234E162000AA379E /* SearchAndDisplayAccessory.swift in Sources */,
|
||||
59158B1621E37B0200D90CB0 /* GridViewController.swift in Sources */,
|
||||
CE5B7CFE245626D3002E5C06 /* Upload.swift in Sources */,
|
||||
CE56CECE209D81DE00932C01 /* AppDelegate.swift in Sources */,
|
||||
CE56CF0D209D83B800932C01 /* ImageSelector.swift in Sources */,
|
||||
CE56CEFF209D83B800932C01 /* CameraController.swift in Sources */,
|
||||
CE56CF05209D83B800932C01 /* ViewControllerExtensions.swift in Sources */,
|
||||
CE56CF0E209D83B800932C01 /* PhotoCaptureHandler.swift in Sources */,
|
||||
CE56CEFE209D83B800932C01 /* RoundedButton.swift in Sources */,
|
||||
CE56CF07209D83B800932C01 /* ImageCell.swift in Sources */,
|
||||
CE56CF06209D83B800932C01 /* CameraView.swift in Sources */,
|
||||
CE56CF0A209D83B800932C01 /* CropView.swift in Sources */,
|
||||
5904C33C2199D0260046A573 /* AlwaysShowPopup.swift in Sources */,
|
||||
CE85AA16246A96C3002D1074 /* UINavigationItem+Extensions.swift in Sources */,
|
||||
CEB269592445DB72004B74B3 /* Database.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 */,
|
||||
E25AAC7E283D855D006E9E7F /* ContentView.swift in Sources */,
|
||||
E2EA00F328438E6B00F7B269 /* CapNameEntryView.swift in Sources */,
|
||||
E25AAC8B283D868D006E9E7F /* Classifier.swift in Sources */,
|
||||
E25AAC94283D88A4006E9E7F /* Cap.swift in Sources */,
|
||||
E2EA00D9283F5BB900F7B269 /* CameraView.swift in Sources */,
|
||||
E2EA00E3283F662800F7B269 /* GridView.swift in Sources */,
|
||||
E2EA00EB284109CC00F7B269 /* CGImage+Extensions.swift in Sources */,
|
||||
E2EA00DF283F5CA000F7B269 /* ErrorView.swift in Sources */,
|
||||
E2EA00D5283EDFA200F7B269 /* FrameManager.swift in Sources */,
|
||||
E25AAC90283D871E006E9E7F /* CGImagePropertyOrientation+Extensions.swift in Sources */,
|
||||
E2EA00CE283EBEB600F7B269 /* SearchField.swift in Sources */,
|
||||
E2EA00C7283EAA0100F7B269 /* SortSelectionView.swift in Sources */,
|
||||
E2EA00DD283F5C6A00F7B269 /* FrameView.swift in Sources */,
|
||||
E2EA00EF28420AA000F7B269 /* Data+Extensions.swift in Sources */,
|
||||
E2EA00C5283EA72000F7B269 /* SortCriteria.swift in Sources */,
|
||||
E25AAC7C283D855D006E9E7F /* CapsApp.swift in Sources */,
|
||||
E2EA00D1283EDD6300F7B269 /* CameraManager.swift in Sources */,
|
||||
E25AAC9B283E3395006E9E7F /* CapRowView.swift in Sources */,
|
||||
E2EA00DB283F5C0600F7B269 /* ContentViewModel.swift in Sources */,
|
||||
E2EA00CC283EB43E00F7B269 /* SortCaseRowView.swift in Sources */,
|
||||
E2EA00E7283F6D0800F7B269 /* URL+Extensions.swift in Sources */,
|
||||
E2EA00D3283EDDF700F7B269 /* CameraError.swift in Sources */,
|
||||
E25AAC92283D8808006E9E7F /* CapData.swift in Sources */,
|
||||
E25AAC96283E14DF006E9E7F /* Database.swift in Sources */,
|
||||
E25AAC8D283D86CF006E9E7F /* Logger.swift in Sources */,
|
||||
E2EA00ED2841170100F7B269 /* UIImage+Extensions.swift in Sources */,
|
||||
E2EA00E5283F69DF00F7B269 /* SettingsStatisticRow.swift in Sources */,
|
||||
E2EA00E1283F658E00F7B269 /* SettingsView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* 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 */
|
||||
CE56CEDA209D81E000932C01 /* Debug */ = {
|
||||
E25AAC84283D855F006E9E7F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
@ -391,7 +329,6 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@ -410,8 +347,9 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
@ -419,14 +357,13 @@
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CE56CEDB209D81E000932C01 /* Release */ = {
|
||||
E25AAC85283D855F006E9E7F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
@ -452,7 +389,6 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
@ -465,8 +401,9 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.5;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
@ -474,47 +411,63 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
CE56CEDD209D81E000932C01 /* Debug */ = {
|
||||
E25AAC87283D855F006E9E7F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Caps/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||
INFOPLIST_FILE = "$(SRCROOT)/Caps/Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
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 = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.4;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.Caps;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE = "";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
CE56CEDE209D81E000932C01 /* Release */ = {
|
||||
E25AAC88283D855F006E9E7F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Caps/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||
INFOPLIST_FILE = "$(SRCROOT)/Caps/Info.plist";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
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 = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.4;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.christophhagen.Caps;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE = "";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 1;
|
||||
};
|
||||
@ -523,20 +476,20 @@
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
CE56CEC5209D81DD00932C01 /* Build configuration list for PBXProject "Caps" */ = {
|
||||
E25AAC73283D855D006E9E7F /* Build configuration list for PBXProject "Caps" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CE56CEDA209D81E000932C01 /* Debug */,
|
||||
CE56CEDB209D81E000932C01 /* Release */,
|
||||
E25AAC84283D855F006E9E7F /* Debug */,
|
||||
E25AAC85283D855F006E9E7F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
CE56CEDC209D81E000932C01 /* Build configuration list for PBXNativeTarget "Caps" */ = {
|
||||
E25AAC86283D855F006E9E7F /* Build configuration list for PBXNativeTarget "Caps" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
CE56CEDD209D81E000932C01 /* Debug */,
|
||||
CE56CEDE209D81E000932C01 /* Release */,
|
||||
E25AAC87283D855F006E9E7F /* Debug */,
|
||||
E25AAC88283D855F006E9E7F /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
@ -544,36 +497,49 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
CE5B7D012458C921002E5C06 /* XCRemoteSwiftPackageReference "Reachability" */ = {
|
||||
E27E15DF283E418600F6804A /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/ashleymills/Reachability.swift";
|
||||
repositoryURL = "https://github.com/lorenzofiamingo/swiftui-cached-async-image";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 5.0.0;
|
||||
minimumVersion = 2.0.0;
|
||||
};
|
||||
};
|
||||
CEB269552445DB56004B74B3 /* XCRemoteSwiftPackageReference "SQLite" */ = {
|
||||
E2EA00C1283E672A00F7B269 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/stephencelis/SQLite.swift";
|
||||
repositoryURL = "https://github.com/SFSafeSymbols/SFSafeSymbols";
|
||||
requirement = {
|
||||
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 */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
CE5B7D022458C921002E5C06 /* Reachability */ = {
|
||||
E27E15E0283E418600F6804A /* CachedAsyncImage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = CE5B7D012458C921002E5C06 /* XCRemoteSwiftPackageReference "Reachability" */;
|
||||
productName = Reachability;
|
||||
package = E27E15DF283E418600F6804A /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */;
|
||||
productName = CachedAsyncImage;
|
||||
};
|
||||
CEB269562445DB56004B74B3 /* SQLite */ = {
|
||||
E2EA00C2283E672A00F7B269 /* SFSafeSymbols */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = CEB269552445DB56004B74B3 /* XCRemoteSwiftPackageReference "SQLite" */;
|
||||
productName = SQLite;
|
||||
package = E2EA00C1283E672A00F7B269 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */;
|
||||
productName = SFSafeSymbols;
|
||||
};
|
||||
E2EA00C9283EACB200F7B269 /* BottomSheet */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E2EA00C8283EACB200F7B269 /* XCRemoteSwiftPackageReference "bottom-sheet" */;
|
||||
productName = BottomSheet;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = CE56CEC2209D81DD00932C01 /* Project object */;
|
||||
rootObject = E25AAC70283D855D006E9E7F /* Project object */;
|
||||
}
|
||||
|
@ -1,25 +1,32 @@
|
||||
{
|
||||
"object": {
|
||||
"pins" : [
|
||||
{
|
||||
"package": "Reachability",
|
||||
"repositoryURL": "https://github.com/ashleymills/Reachability.swift",
|
||||
"identity" : "bottom-sheet",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/weitieda/bottom-sheet",
|
||||
"state" : {
|
||||
"branch": null,
|
||||
"revision": "c01bbdf2d633cf049ae1ed1a68a2020a8bda32e2",
|
||||
"version": "5.1.0"
|
||||
"revision" : "4e074d49f3148577ac66cf47b85a99d016480d01",
|
||||
"version" : "1.0.10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SQLite.swift",
|
||||
"repositoryURL": "https://github.com/stephencelis/SQLite.swift",
|
||||
"identity" : "sfsafesymbols",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/SFSafeSymbols/SFSafeSymbols",
|
||||
"state" : {
|
||||
"branch": null,
|
||||
"revision": "5f5ad81ac0d0a0f3e56e39e646e8423c617df523",
|
||||
"version": "0.13.2"
|
||||
"revision" : "c8c33d947d8a1c883aa19fd24e14fd738b06e369",
|
||||
"version" : "3.3.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"?>
|
||||
<Bucket
|
||||
uuid = "B1F05379-5C9B-42D8-94ED-BB89F9571BE1"
|
||||
uuid = "B7EB9D95-1101-4B5E-A288-5C0C223F7E4C"
|
||||
type = "1"
|
||||
version = "2.0">
|
||||
</Bucket>
|
@ -4,52 +4,18 @@
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>CapCollector.xcscheme_^#shared#^_</key>
|
||||
<key>Caps.xcscheme_^#shared#^_</key>
|
||||
<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>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>SQLite (Playground) 4.xcscheme</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>5</integer>
|
||||
</dict>
|
||||
<key>SQLite (Playground) 5.xcscheme</key>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>6</integer>
|
||||
</dict>
|
||||
<key>SQLite (Playground).xcscheme</key>
|
||||
<key>E25AAC77283D855D006E9E7F</key>
|
||||
<dict>
|
||||
<key>isShown</key>
|
||||
<false/>
|
||||
<key>orderHint</key>
|
||||
<integer>2</integer>
|
||||
<key>primary</key>
|
||||
<true/>
|
||||
</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" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "bottle-cap40.png",
|
||||
"scale" : "2x"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "bottle-cap60.png",
|
||||
"scale" : "3x"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "bottle-cap58.png",
|
||||
"scale" : "2x"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "bottle-cap87.png",
|
||||
"scale" : "3x"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "bottle-cap80.png",
|
||||
"scale" : "2x"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "bottle-cap120.png",
|
||||
"scale" : "3x"
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "bottle-cap120-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "bottle-cap180.png",
|
||||
"scale" : "3x"
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "bottle-cap180-1.png",
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "1x"
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "1x"
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "1x"
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "76x76",
|
||||
"scale" : "1x"
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "76x76",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "83.5x83.5",
|
||||
"scale" : "2x"
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "bottle-cap1024.png",
|
||||
"scale" : "1x"
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"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" : {
|
||||
"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 UIKit
|
||||
import CoreImage
|
||||
|
||||
import SQLite
|
||||
|
||||
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
|
||||
let id: Int
|
||||
|
||||
/// The name of the cap
|
||||
let name: String
|
||||
var name: String
|
||||
|
||||
/// The name of the cap without special characters
|
||||
let cleanName: String
|
||||
var cleanName: String
|
||||
|
||||
/// 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
|
||||
let matched: Bool
|
||||
/// The index of the main image for the cap
|
||||
var mainImage: Int
|
||||
|
||||
/// Indicate if the cap is present on the server
|
||||
let uploaded: Bool
|
||||
/// The version of the first classifier capable of recognizing the cap
|
||||
var classifierVersion: Int?
|
||||
|
||||
// MARK: Init
|
||||
var color: Color?
|
||||
|
||||
init(name: String, id: Int) {
|
||||
self.id = id
|
||||
self.count = 1
|
||||
self.name = name
|
||||
self.cleanName = ""
|
||||
self.matched = false
|
||||
self.uploaded = false
|
||||
/// The subpath to the main image on the server
|
||||
var mainImagePath: String {
|
||||
String(format: "images/%04d/%04d-%02d.jpg", id, id, mainImage)
|
||||
}
|
||||
|
||||
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.name = name
|
||||
self.count = count
|
||||
self.cleanName = name.clean
|
||||
self.matched = false
|
||||
self.uploaded = true
|
||||
self.imageCount = 1
|
||||
self.mainImage = 0
|
||||
self.classifierVersion = classifier
|
||||
}
|
||||
|
||||
func renamed(to name: String) -> Cap {
|
||||
Cap(from: self, renamed: name)
|
||||
init(data: CapData) {
|
||||
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) {
|
||||
self.id = cap.id
|
||||
self.count = cap.count
|
||||
self.name = newName
|
||||
self.cleanName = newName.clean
|
||||
self.matched = cap.matched
|
||||
self.uploaded = cap.uploaded
|
||||
mutating func update(with data: CapData) {
|
||||
self.name = data.name
|
||||
self.cleanName = data.name.clean
|
||||
self.imageCount = data.count
|
||||
self.mainImage = data.mainImage
|
||||
self.classifierVersion = data.classifierVersion
|
||||
}
|
||||
|
||||
// MARK: SQLite
|
||||
|
||||
init(row: Row) {
|
||||
self.id = row[Cap.columnId]
|
||||
self.name = row[Cap.columnName]
|
||||
self.count = row[Cap.columnCount]
|
||||
self.cleanName = name.clean
|
||||
self.matched = row[Cap.columnMatched]
|
||||
self.uploaded = row[Cap.columnUploaded]
|
||||
|
||||
static func ==(lhs: Cap, rhs: CapData) -> Bool {
|
||||
lhs.id == rhs.id &&
|
||||
lhs.name == rhs.name &&
|
||||
lhs.imageCount == rhs.count &&
|
||||
lhs.mainImage == rhs.mainImage &&
|
||||
lhs.classifierVersion == rhs.classifierVersion
|
||||
}
|
||||
|
||||
static let table = Table("data")
|
||||
static func !=(lhs: Cap, rhs: CapData) -> Bool {
|
||||
!(lhs == rhs)
|
||||
}
|
||||
|
||||
static var createQuery: String {
|
||||
table.create(ifNotExists: true) { t in
|
||||
t.column(columnId, primaryKey: true)
|
||||
t.column(columnName)
|
||||
t.column(columnCount)
|
||||
t.column(columnMatched)
|
||||
t.column(columnUploaded)
|
||||
func classifiable(by classifierVersion: Int?) -> Bool {
|
||||
guard let version = classifierVersion else {
|
||||
return false
|
||||
}
|
||||
guard let own = self.classifierVersion else {
|
||||
return false
|
||||
}
|
||||
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")
|
||||
|
||||
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))
|
||||
let b: Int
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Protocol Hashable
|
||||
|
||||
extension Cap: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Protocol Logger
|
||||
// MARK: String extension
|
||||
|
||||
extension Cap: Logger { }
|
||||
|
||||
// MARK: - String extension
|
||||
|
||||
extension String {
|
||||
private extension String {
|
||||
|
||||
var clean: String {
|
||||
return lowercased().replacingOccurrences(of: "[^a-z0-9 ]", with: "", options: .regularExpression)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Int extension
|
||||
// MARK: Int extension
|
||||
|
||||
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 Vision
|
||||
import CoreML
|
||||
@ -24,17 +16,13 @@ class Classifier: Logger {
|
||||
/**
|
||||
Classify an image
|
||||
- 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.
|
||||
*/
|
||||
func recognize(image: UIImage, completion: @escaping (_ matches: [Int: Float]?) -> Void) {
|
||||
guard let ciImage = CIImage(image: image) else {
|
||||
error("Unable to create CIImage")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let orientation = CGImagePropertyOrientation(image.imageOrientation)
|
||||
let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
|
||||
func recognize(image: CGImage, completion: @escaping (_ matches: [Int: Float]?) -> Void) {
|
||||
let image = CIImage(cgImage: image)
|
||||
let handler = VNImageRequestHandler(ciImage: image, orientation: .up)
|
||||
let request = VNCoreMLRequest(model: model) { request, error in
|
||||
let matches = self.process(request: request, error: error)
|
||||
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
|
||||
|
||||
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 UIKit
|
||||
|
||||
@ -22,7 +14,7 @@ extension UIImage {
|
||||
func resize(to targetSize: CGSize) -> UIImage {
|
||||
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)
|
||||
let newImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
@ -35,7 +27,7 @@ extension UIImage {
|
||||
- returns: The cropped image
|
||||
*/
|
||||
func crop(factor: CGFloat) -> UIImage {
|
||||
let width = self.size.width * factor
|
||||
let width = size.width * scale * factor
|
||||
return crop(to: width)
|
||||
}
|
||||
|
||||
@ -45,15 +37,11 @@ extension UIImage {
|
||||
- returns: The cropped image
|
||||
*/
|
||||
func crop(to size: CGFloat) -> UIImage {
|
||||
var rect = CGRect(
|
||||
x: (self.size.height - size) / 2,
|
||||
y: (self.size.width - size) / 2,
|
||||
width: size,
|
||||
height: size)
|
||||
rect.origin.x *= scale
|
||||
rect.origin.y *= scale
|
||||
rect.size.width *= scale
|
||||
rect.size.height *= scale
|
||||
let rect = CGRect(
|
||||
x: (self.size.height * scale - size) / 2,
|
||||
y: (self.size.width * scale - size) / 2,
|
||||
width: size * scale,
|
||||
height: size * scale)
|
||||
|
||||
let imageRef = cgImage!.cropping(to: rect)
|
||||
return UIImage(cgImage: imageRef!, scale: scale, orientation: imageOrientation)
|
||||
@ -77,7 +65,7 @@ extension UIImage {
|
||||
return nil
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
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))
|
||||
}
|
||||
}
|