Download classifier, database

This commit is contained in:
christophhagen
2020-05-16 11:21:55 +02:00
parent dceb3ca07d
commit 7287607a60
50 changed files with 3555 additions and 2919 deletions

View File

@ -18,29 +18,31 @@ class CapCell: UITableViewCell {
@IBOutlet private weak var nameLabel: UILabel!
@IBOutlet weak var countLabel: UILabel!
var id = 0
var cap: Cap! {
didSet {
updateCell()
}
}
func updateCell() {
capImage.image = cap.image
var id: Int = 0
func set(image: UIImage?) {
capImage.image = image ?? UIImage(named: "launch")
}
func set(cap: Cap, match: Float?) {
id = cap.id
if let image = cap.image {
set(image: image)
} else {
capImage.image = UIImage(named: "launch")
cap.downloadMainImage() { image in
self.set(image: image)
}
}
//capImage.borderColor = AppDelegate.tintColor
matchLabel.text = text(for: cap.match)
matchLabel.text = cap.matchDescription(match: match)
nameLabel.text = cap.name
countLabel.text = "\(cap.id) (\(cap.count) image" + (cap.count > 1 ? "s)" : ")")
countLabel.text = cap.subtitle
}
private func text(for value: Float?) -> String? {
guard let nr = value else {
return nil
}
let percent = Int((nr * 100).rounded())
return String(format: "%d %%", arguments: [percent])
}
}

View File

@ -10,8 +10,10 @@ import UIKit
class GridViewController: UIViewController {
/// The number of caps horizontally.
private let columns = 40
/// The number of hroizontal pixels for each cap.
static let len: CGFloat = 60
private lazy var rowHeight = GridViewController.len * 0.866
@ -24,6 +26,13 @@ class GridViewController: UIViewController {
@IBOutlet weak var scrollView: UIScrollView!
/// A dictionary of the caps for the tiles
private var tiles = [Cap]()
private var installedTiles = [Int : RoundedImageView]()
private var changedTiles = Set<Int>()
private var selectedTile: Int? = nil
private weak var selectionView: RoundedButton!
@ -38,13 +47,21 @@ class GridViewController: UIViewController {
private var isShowingColors = false
private var capCount = 0
@IBAction func toggleAverageColor(_ sender: Any) {
isShowingColors = !isShowingColors
for (tile, view) in installedTiles {
if isShowingColors {
view.image = nil
} else {
view.image = Cap.tileImage(tile: tile)
if let image = tiles[tile].thumbnail {
view.image = image
continue
}
tiles[tile].downloadMainImage() { image in
view.image = image
}
}
}
}
@ -52,12 +69,15 @@ class GridViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
app.database.add(listener: self)
capCount = app.database.capCount
tiles = app.database.caps.sorted { $0.tile < $1.tile }
let width = CGFloat(columns) * GridViewController.len + GridViewController.len / 2
let height = (CGFloat(Cap.totalCapCount) / CGFloat(columns)).rounded(.up) * rowHeight + margin
let height = (CGFloat(capCount) / CGFloat(columns)).rounded(.up) * rowHeight + margin
canvasSize = CGSize(width: width, height: height)
myView = UIView(frame: CGRect(origin: .zero, size: canvasSize))
scrollView.addSubview(myView)
scrollView.contentSize = canvasSize
scrollView.delegate = self
@ -82,10 +102,36 @@ class GridViewController: UIViewController {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
Cap.save()
saveChangedTiles()
}
// MARK: Tiles
private func tileColor(tile: Int) -> UIColor {
return tiles[tile].color
}
private func saveChangedTiles() {
for tile in changedTiles {
let cap = tiles[tile]
app.database.update(tile: tile, for: cap.id)
}
}
/**
Switch two tiles.
*/
private func switchTiles(_ lhs: Int, _ rhs: Int) -> Bool {
let temp = tiles[rhs]
tiles[rhs] = tiles[lhs]
tiles[lhs] = temp
changedTiles.insert(lhs)
changedTiles.insert(rhs)
return true
}
private func setZoomRange() {
let size = scrollView.frame.size
let a = size.width / canvasSize.width
@ -106,11 +152,16 @@ class GridViewController: UIViewController {
guard s > margin else {
return
}
let column: CGFloat
let column: Int
if row.isEven {
column = loc.x / GridViewController.len
column = Int(loc.x / GridViewController.len)
// Abort, if user tapped outside of the grid
if column >= columns {
clearTileSelection()
return
}
} else {
column = (loc.x - GridViewController.len / 2) / GridViewController.len
column = Int((loc.x - GridViewController.len / 2) / GridViewController.len)
}
handleTileTapped(tile: row * columns + Int(column))
}
@ -123,8 +174,6 @@ class GridViewController: UIViewController {
}
}
private var installedTiles = [Int : RoundedImageView]()
private func showSelection(tile: Int) {
clearTileSelection()
@ -144,13 +193,23 @@ class GridViewController: UIViewController {
private func makeTile(_ tile: Int) {
let view = RoundedImageView(frame: frame(for: tile))
myView.addSubview(view)
view.backgroundColor = Cap.tileColor(tile: tile)
view.backgroundColor = tileColor(tile: tile)
defer {
installedTiles[tile] = view
}
// Only set image if images are shown
if !isShowingColors {
view.image = Cap.tileImage(tile: tile)
guard !isShowingColors else {
return
}
if let image = tiles[tile].thumbnail {
view.image = image
return
}
installedTiles[tile] = view
tiles[tile].downloadMainImage() { image in
view.image = image
}
}
private func frame(for tile: Int) -> CGRect {
@ -166,13 +225,18 @@ class GridViewController: UIViewController {
clearTileSelection()
return
}
Cap.switchTiles(oldTile, newTile)
guard switchTiles(oldTile, newTile) else {
clearTileSelection()
return
}
// Switch cap colors
installedTiles[oldTile]?.backgroundColor = Cap.tileColor(tile: oldTile)
installedTiles[newTile]?.backgroundColor = Cap.tileColor(tile: newTile)
let temp = installedTiles[oldTile]?.backgroundColor
installedTiles[oldTile]?.backgroundColor = installedTiles[newTile]?.backgroundColor
installedTiles[newTile]?.backgroundColor = temp
if !isShowingColors {
installedTiles[oldTile]?.image = Cap.tileImage(tile: oldTile)
installedTiles[newTile]?.image = Cap.tileImage(tile: newTile)
let temp = installedTiles[oldTile]?.image
installedTiles[oldTile]?.image = installedTiles[newTile]?.image
installedTiles[newTile]?.image = temp
}
clearTileSelection()
@ -187,32 +251,46 @@ class GridViewController: UIViewController {
}
private func showTiles(in rect: CGRect) {
for i in 0..<Cap.totalCapCount {
if tileIsVisible(tile: i, in: rect) {
if installedTiles[i] != nil {
continue
}
makeTile(i)
} else if let tile = installedTiles[i] {
tile.removeFromSuperview()
installedTiles[i] = nil
}
for tile in 0..<capCount {
refresh(tile: tile, inVisibleRect: rect)
}
}
private func updateTiles() {
guard #available(iOS 12.0, *) else {
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)
let rect = CGRect(origin: scaledOrigin, size: scaledSize)
showTiles(in: rect)
return CGRect(origin: scaledOrigin, size: scaledSize)
}
private func updateTiles() {
DispatchQueue.main.async {
self.showTiles(in: self.visibleRect)
}
}
}
@ -234,8 +312,31 @@ private extension Int {
}
}
extension GridViewController: Logger {
extension GridViewController: Logger { }
extension GridViewController: DatabaseDelegate {
static let logToken: String = "[Grid]"
func database(didChangeCap id: Int) {
guard let view = installedTiles[id] else {
return
}
guard let cap = app.database.cap(for: id) else {
return
}
tiles[cap.tile] = cap
view.backgroundColor = cap.color
// Only set image if images are shown
if !isShowingColors {
view.image = cap.image
}
}
func database(didAddCap cap: Cap) {
tiles.append(cap)
refresh(tile: cap.tile, inVisibleRect: visibleRect)
}
func databaseRequiresFullRefresh() {
updateTiles()
}
}

View File

@ -7,7 +7,6 @@
//
import UIKit
import SwiftyDropbox
class ImageSelector: UIViewController {
@ -28,13 +27,25 @@ class ImageSelector: UIViewController {
return false
}
// MARK: - CollectionView
// MARK: - Variables
private var titleLabel: UILabel!
private var subtitleLabel: UILabel!
private var images = [UIImage?]()
var cap: Cap!
@IBOutlet weak var collection: UICollectionView!
private var titleText: String {
"Cap \(cap.id) (\(cap.count) images)"
}
private var subtitleText: String {
cap.name
}
// MARK: - Life cycle
@ -43,6 +54,7 @@ class ImageSelector: UIViewController {
collection.dataSource = self
collection.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
@ -50,18 +62,46 @@ class ImageSelector: UIViewController {
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)
event("\(cap.count) images for cap \(cap.id)")
log("\(cap.count) images for cap \(cap.id)")
if let image = cap.image {
self.images[0] = image
self.collection.reloadItems(at: [IndexPath(row: 0, section: 0)])
} else {
cap.downloadImage { mainImage in
self.images[0] = mainImage
cap.downloadMainImage { image in
self.images[0] = image
self.collection.reloadItems(at: [IndexPath(row: 0, section: 0)])
}
}
@ -69,7 +109,10 @@ class ImageSelector: UIViewController {
return
}
for number in 1..<cap.count {
cap.downloadImage(number) { image in
_ = cap.downloadImage(number) { image in
guard let image = image else {
return
}
self.images[number] = image
self.collection.reloadItems(at: [IndexPath(row: number, section: 0)])
}
@ -79,11 +122,7 @@ class ImageSelector: UIViewController {
// MARK: Select
private func selectedImage(nr: Int) {
guard let image = images[nr] else {
return
}
cap.setMainImage(to: nr, image: image)
app.database.setMainImage(of: cap.id, to: nr)
}
}
@ -104,7 +143,7 @@ extension ImageSelector: UICollectionViewDataSource {
withReuseIdentifier: "Image",
for: indexPath) as! ImageCell
cell.capView.image = images[indexPath.row]
cell.capView.image = images[indexPath.row] ?? UIImage(named: "launch")
return cell
}
@ -138,9 +177,6 @@ extension ImageSelector : UICollectionViewDelegateFlowLayout {
}
}
extension ImageSelector: Logger {
static let logToken = "[ImageSelector]"
}
extension ImageSelector: Logger { }

View File

@ -0,0 +1,185 @@
//
// SearchAndDisplayAccessory.swift
// CapCollector
//
// Created by Christoph on 09.10.19.
// Copyright © 2019 CH. All rights reserved.
//
import UIKit
class PassthroughView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
return view == self ? nil : view
}
}
protocol CapAccessoryDelegate: class {
func capSearchWasDismissed()
func capSearch(didChange text: String)
func capAccessoryDidDiscardImage()
func capAccessory(shouldSave image: UIImage)
func capAccessoryCameraButtonPressed()
}
class SearchAndDisplayAccessory: PassthroughView {
// MARK: - Outlets
@IBOutlet weak var newImageView: PassthroughView!
@IBOutlet weak var capImage: RoundedImageView!
@IBOutlet weak var saveButton: UIButton!
@IBOutlet weak var deleteButton: UIButton!
@IBOutlet weak var cameraButton: UIButton!
@IBOutlet weak var searchBar: UISearchBar!
// MARK: - Actions
@IBAction func cameraButtonPressed() {
delegate?.capAccessoryCameraButtonPressed()
}
@IBAction func saveButtonPressed() {
if let image = capImage.image {
delegate?.capAccessory(shouldSave: image)
}
}
@IBAction func cancelButtonPressed() {
discardImage()
}
// MARK: - Variables
var view: UIView?
weak var blurView: UIVisualEffectView?
weak var currentBlurContraint: NSLayoutConstraint?
weak var delegate: CapAccessoryDelegate?
var currentImage: UIImage? {
capImage.image
}
// MARK: - Setup
convenience init(width: CGFloat) {
let frame = CGRect(origin: .zero, size: CGSize(width: width, height: 145))
self.init(frame: frame)
}
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
view = fromNib()
view!.frame = bounds
//view!.autoresizingMask = .flexibleHeight
addSubview(view!)
let blur = UIBlurEffect(style: .systemThinMaterial)
let blurView = UIVisualEffectView(effect: blur)
self.blurView = blurView
blurView.translatesAutoresizingMaskIntoConstraints = false
blurView.isUserInteractionEnabled = false
insertSubview(blurView, at: 0)
let t = searchBar.topAnchor.constraint(equalTo: blurView.topAnchor)
let b = searchBar.bottomAnchor.constraint(equalTo: blurView.bottomAnchor)
let l = leadingAnchor.constraint(equalTo: blurView.leadingAnchor)
let r = trailingAnchor.constraint(equalTo: blurView.trailingAnchor)
addConstraints([t, b, l, r])
currentBlurContraint = t
self.newImageView.alpha = 0
self.newImageView.isHidden = true
searchBar.text = nil
searchBar.setShowsCancelButton(false, animated: false)
searchBar.delegate = self
cameraButton.setImage(UIImage.templateImage(named: "camera_square"), for: .normal)
}
// MARK: Search bar
func dismissAndClearSearchBar() {
searchBar.resignFirstResponder()
searchBar.text = nil
}
// MARK: Cap image
func showImageView(with image: UIImage) {
capImage.image = image
showImageView()
}
func discardImage() {
dismissAndClearSearchBar()
hideImageView()
delegate?.capAccessoryDidDiscardImage()
}
func hideImageView() {
currentBlurContraint?.isActive = false
let t = searchBar.topAnchor.constraint(equalTo: blurView!.topAnchor)
addConstraint(t)
currentBlurContraint = t
self.newImageView.alpha = 0
self.newImageView.isHidden = true
self.capImage.image = nil
}
private func showImageView() {
currentBlurContraint?.isActive = false
let t = blurView!.topAnchor.constraint(equalTo: saveButton.topAnchor, constant: -8)
addConstraint(t)
currentBlurContraint = t
self.newImageView.isHidden = false
self.newImageView.alpha = 1
}
}
// MARK: - UISearchBarDelegate
extension SearchAndDisplayAccessory: UISearchBarDelegate {
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
searchBar.resignFirstResponder()
searchBar.text = nil
delegate?.capSearchWasDismissed()
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
searchBar.resignFirstResponder()
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
delegate?.capSearch(didChange: searchText)
}
}

View File

@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16096" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16086"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SearchAndDisplayAccessory" customModule="CapCollector" customModuleProvider="target">
<connections>
<outlet property="cameraButton" destination="bIA-eq-Tn5" id="r0F-0j-Ve9"/>
<outlet property="capImage" destination="vQm-nH-J8o" id="bQK-Vu-Z1U"/>
<outlet property="deleteButton" destination="qhB-Sd-K8H" id="oym-9o-1m3"/>
<outlet property="newImageView" destination="0wK-yR-rO9" id="Gi3-I6-Xv9"/>
<outlet property="saveButton" destination="dt5-LD-28a" id="IYT-eN-3lb"/>
<outlet property="searchBar" destination="bCh-7y-t0w" id="8Tt-4h-Fkg"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="PassthroughView" customModule="CapCollector" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="414" height="145"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<searchBar contentMode="redraw" searchBarStyle="minimal" placeholder="Search caps" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bCh-7y-t0w">
<rect key="frame" x="0.0" y="89" width="358" height="56"/>
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" keyboardAppearance="alert" returnKeyType="search" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no"/>
<scopeButtonTitles>
<string>Title</string>
<string>Title</string>
</scopeButtonTitles>
</searchBar>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="bIA-eq-Tn5">
<rect key="frame" x="358" y="89" width="56" height="56"/>
<constraints>
<constraint firstAttribute="width" secondItem="bIA-eq-Tn5" secondAttribute="height" multiplier="1:1" id="O09-ww-bHE"/>
<constraint firstAttribute="height" constant="56" id="Y3N-l5-d94"/>
</constraints>
<inset key="imageEdgeInsets" minX="5" minY="5" maxX="5" maxY="5"/>
<state key="normal" image="camera_square"/>
<connections>
<action selector="cameraButtonPressed" destination="-1" eventType="touchUpInside" id="ooo-b8-Atj"/>
</connections>
</button>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="0wK-yR-rO9">
<rect key="frame" x="0.0" y="-1" width="414" height="90"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="vQm-nH-J8o" customClass="RoundedImageView" customModule="CapCollector" customModuleProvider="target">
<rect key="frame" x="2" y="2" width="86" height="86"/>
<constraints>
<constraint firstAttribute="width" secondItem="vQm-nH-J8o" secondAttribute="height" multiplier="1:1" id="WHb-tV-k4S"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="color" keyPath="borderColor">
<color key="value" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="number" keyPath="borderWidth">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</imageView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="dt5-LD-28a">
<rect key="frame" x="96" y="60" width="151" height="30"/>
<constraints>
<constraint firstAttribute="height" constant="30" id="Boy-dJ-2BJ"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Save"/>
<connections>
<action selector="saveButtonPressed" destination="-1" eventType="touchUpInside" id="O49-6L-mNY"/>
</connections>
</button>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="qhB-Sd-K8H">
<rect key="frame" x="255" y="58.5" width="151" height="33"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<state key="normal" title="Delete"/>
<connections>
<action selector="cancelButtonPressed" destination="-1" eventType="touchUpInside" id="4Sm-nV-aOd"/>
</connections>
</button>
</subviews>
<constraints>
<constraint firstItem="vQm-nH-J8o" firstAttribute="leading" secondItem="0wK-yR-rO9" secondAttribute="leading" constant="2" id="6q2-Lc-eIF"/>
<constraint firstItem="dt5-LD-28a" firstAttribute="leading" secondItem="vQm-nH-J8o" secondAttribute="trailing" constant="8" id="OUT-X8-cxn"/>
<constraint firstAttribute="bottom" secondItem="vQm-nH-J8o" secondAttribute="bottom" constant="2" id="P8h-BA-8Yc"/>
<constraint firstAttribute="height" constant="90" id="Rsk-qm-0WZ"/>
<constraint firstAttribute="trailing" secondItem="qhB-Sd-K8H" secondAttribute="trailing" constant="8" id="fzn-7b-WrO"/>
<constraint firstItem="vQm-nH-J8o" firstAttribute="top" secondItem="0wK-yR-rO9" secondAttribute="top" constant="2" id="h1D-Ut-lnp"/>
<constraint firstAttribute="bottom" secondItem="dt5-LD-28a" secondAttribute="bottom" id="wFi-0u-ian"/>
<constraint firstItem="qhB-Sd-K8H" firstAttribute="leading" secondItem="dt5-LD-28a" secondAttribute="trailing" constant="8" id="y1c-g7-5zg"/>
<constraint firstItem="qhB-Sd-K8H" firstAttribute="centerY" secondItem="dt5-LD-28a" secondAttribute="centerY" id="yhy-DU-ReF"/>
<constraint firstItem="qhB-Sd-K8H" firstAttribute="width" secondItem="dt5-LD-28a" secondAttribute="width" id="zpv-6V-gXr"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="bIA-eq-Tn5" secondAttribute="bottom" id="Geo-F0-pdI"/>
<constraint firstItem="bCh-7y-t0w" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="M9M-He-vxM"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="top" secondItem="0wK-yR-rO9" secondAttribute="top" constant="1" id="Uyp-J7-bKr"/>
<constraint firstItem="bIA-eq-Tn5" firstAttribute="leading" secondItem="bCh-7y-t0w" secondAttribute="trailing" id="i7w-LO-9lv"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="0wK-yR-rO9" secondAttribute="trailing" id="iWS-vI-RAT"/>
<constraint firstItem="bCh-7y-t0w" firstAttribute="top" secondItem="0wK-yR-rO9" secondAttribute="bottom" id="iab-RT-0nN"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="bIA-eq-Tn5" secondAttribute="trailing" id="kQV-mX-EoD"/>
<constraint firstItem="vUN-kp-3ea" firstAttribute="bottom" secondItem="bCh-7y-t0w" secondAttribute="bottom" id="r8c-kO-KT5"/>
<constraint firstItem="0wK-yR-rO9" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="rd3-iN-jtD"/>
</constraints>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
<point key="canvasLocation" x="97.5" y="105.625"/>
</view>
</objects>
<resources>
<image name="camera_square" width="220" height="220"/>
</resources>
</document>

View File

@ -1,272 +0,0 @@
//
// SettingsController.swift
// CapCollector
//
// Created by Christoph on 15.10.18.
// Copyright © 2018 CH. All rights reserved.
//
import UIKit
class SettingsController: UITableViewController {
@IBOutlet weak var totalCapsLabel: UILabel!
@IBOutlet weak var totalImagesLabel: UILabel!
@IBOutlet weak var recognizedCapsLabel: UILabel!
@IBOutlet weak var imagesStatsLabel: UILabel!
@IBOutlet weak var databaseUpdatesLabel: UILabel!
@IBOutlet weak var dropboxAccountLabel: UILabel!
@IBOutlet weak var countsLabel: UILabel!
private var isUpdatingCounts = false
private var isUploadingNameFile = false
private var isUpdatingThumbnails = false
private var isUpdatingColors = false
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
updateDropboxStatus()
updateNameFileStats()
updateDatabaseStats()
(navigationController as! NavigationController).allowLandscape = false
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setClassifierChoice(Persistence.useMobileNet)
}
private func updateThumbnails() {
isUpdatingThumbnails = true
for cap in Cap.all.values {
cap.makeThumbnail()
}
isUpdatingThumbnails = false
}
private func updateColors() {
isUpdatingColors = true
Cap.shouldSave = false
for cap in Cap.all.values {
cap.makeAverageColor()
}
Cap.shouldSave = true
isUpdatingColors = false
}
private func updateDatabaseStats() {
let totalCaps = Cap.totalCapCount
totalCapsLabel.text = "\(totalCaps) caps"
totalImagesLabel.text = "\(Cap.imageCount) images"
let recognizedCaps = Persistence.recognizedCapCount
let newCapCount = totalCaps - recognizedCaps
recognizedCapsLabel.text = "\(newCapCount) new caps"
let ratio = Float(Cap.imageCount)/Float(Cap.totalCapCount)
let (images, count) = Cap.getCapStatistics().enumerated().first(where: { $0.element != 0 })!
imagesStatsLabel.text = String(format: "%.2f images per cap, lowest count: %d (%dx)", ratio, images, count)
}
private func updateNameFileStats() {
let capCount = Cap.totalCapCount - Persistence.lastUploadedCapCount
let imageCount = Cap.imageCount - Persistence.lastUploadedImageCount
databaseUpdatesLabel.text = "\(capCount) new caps and \(imageCount) new images"
}
private func updateDropboxStatus() {
dropboxAccountLabel.text = DropboxController.shared.isEnabled ? "Sign out" : "Sign in"
}
private func setClassifierChoice(_ useMobileNet: Bool) {
tableView.cellForRow(at: IndexPath(row: 0, section: 0))?.accessoryType = useMobileNet ? .checkmark : .none
}
private func toggleClassifier() {
let newValue = !Persistence.useMobileNet
Persistence.useMobileNet = newValue
setClassifierChoice(newValue)
}
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
switch indexPath.section {
case 0: // Choose models
return true
case 1: // Mosaic
return true
case 2: // Database
return indexPath.row == 2 && !isUploadingNameFile
case 3: // Refresh
switch indexPath.row {
case 0:
return !isUpdatingCounts
case 1:
return !isUpdatingThumbnails
case 2:
return !isUpdatingColors
default:
return false
}
case 4: // Dropbox account
return true
case 5: // Log file
return true
default: return false
}
}
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
switch indexPath.section {
case 0: // Choose models
return indexPath
case 1: // Mosaic
return indexPath
case 2: // Database
return (indexPath.row == 2 && !isUploadingNameFile) ? indexPath : nil
case 3: // Refresh count
switch indexPath.row {
case 0:
return isUpdatingCounts ? nil : indexPath
case 1:
return isUpdatingThumbnails ? nil : indexPath
case 2:
return isUpdatingColors ? nil : indexPath
default:
return nil
}
case 4: // Dropbox account
return indexPath
case 5: // Log file
return indexPath
default: return nil
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch indexPath.section {
case 0: // Choose models
toggleClassifier()
case 2: // Upload
if indexPath.row == 2 && !isUploadingNameFile {
uploadNameFile()
}
case 3: // Refresh count
switch indexPath.row {
case 0:
updateCounts()
case 1:
updateThumbnails()
case 2:
updateColors()
default:
break
}
default:
break
}
}
private func updateCounts() {
isUpdatingCounts = true
Cap.shouldSave = false
// TODO: Don't make all requests at the same time
DispatchQueue.global(qos: .userInitiated).async {
let list = Cap.all.keys.sorted()
let total = list.count
var finished = 0
let chunks = list.chunked(into: 10)
for chunk in chunks {
self.updateCounts(ids: chunk)
finished += 10
DispatchQueue.main.async {
self.countsLabel.text = "\(finished) / \(total) finished"
}
}
self.isUpdatingCounts = false
Cap.shouldSave = true
DispatchQueue.main.async {
self.countsLabel.text = "Refresh image counts"
self.updateDatabaseStats()
}
}
}
private func updateCounts(ids: [Int]) {
var count = ids.count
let s = DispatchSemaphore(value: 0)
for cap in ids {
Cap.all[cap]!.updateCount { _ in
count -= 1
if count == 0 {
s.signal()
}
}
}
_ = s.wait(timeout: .now() + .seconds(30))
event("Finished updating ids \(ids.first!) to \(ids.last!)")
}
private func uploadNameFile() {
event("Uploading name file")
isUploadingNameFile = true
Cap.saveAndUpload() { _ in
self.isUploadingNameFile = false
self.updateNameFileStats()
}
}
private func toggleDropbox() {
guard !DropboxController.shared.isEnabled else {
DropboxController.shared.signOut()
updateDropboxStatus()
return
}
DropboxController.shared.setup(in: self)
updateDropboxStatus()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let id = segue.identifier else {
return
}
switch id {
case "showMosaic":
(navigationController as! NavigationController).allowLandscape = true
case "showLog":
return
default:
return
}
}
}
extension SettingsController: Logger {
static let logToken = "[Settings]"
}
private extension Array {
func chunked(into size: Int) -> [[Element]] {
return stride(from: 0, to: count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)])
}
}
}

View File

@ -15,7 +15,9 @@ enum SortCriteria: Int {
case match = 3
}
protocol SortControllerDelegate {
protocol SortControllerDelegate: class {
var sortControllerShouldIncludeMatchOption: Bool { get }
func sortController(didSelect sortType: SortCriteria, ascending: Bool)
}
@ -26,32 +28,19 @@ class SortController: UITableViewController {
var ascending: Bool = true
var delegate: SortControllerDelegate?
private var includeMatches: Bool {
delegate?.sortControllerShouldIncludeMatchOption ?? false
}
weak var delegate: SortControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
let height = Cap.hasMatches ? 310 : 270
let height = includeMatches ? 298 : 258
preferredContentSize = CGSize(width: 200, height: height)
}
private func setCell() {
for i in 0..<4 {
let index = IndexPath(row: i, section: 1)
let cell = tableView.cellForRow(at: index)
cell?.accessoryType = i == selected.rawValue ? .checkmark : .none
}
let index = IndexPath(row: 0, section: 0)
let cell = tableView.cellForRow(at: index)
cell?.accessoryType = ascending ? .checkmark : .none
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setCell()
}
private func giveFeedback(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
let generator = UIImpactFeedbackGenerator(style: style)
generator.impactOccurred()
@ -63,7 +52,7 @@ class SortController: UITableViewController {
tableView.deselectRow(at: indexPath, animated: true)
guard indexPath.section == 1 else {
ascending = !ascending
setCell()
tableView.reloadData()
delegate?.sortController(didSelect: selected, ascending: ascending)
giveFeedback(.light)
return
@ -76,17 +65,16 @@ class SortController: UITableViewController {
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
guard indexPath.row == 3 else { return indexPath }
return Cap.hasMatches ? indexPath : nil
return includeMatches ? indexPath : nil
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
switch indexPath.section {
case 0:
cell.accessoryType = ascending ? .checkmark : .none
default:
cell.accessoryType = indexPath.row == selected.rawValue ? .checkmark : .none
break
}
}
*/
}