2019-03-15 13:19:19 +01:00
//
// T a b l e V i e w . s w i f t
// C a p F i n d e r
//
// C r e a t e d b y U s e r o n 2 2 . 0 4 . 1 8 .
// C o p y r i g h t © 2 0 1 8 U s e r . A l l r i g h t s r e s e r v e d .
//
import UIKit
2020-05-16 11:21:55 +02:00
import JGProgressHUD
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
class TableView : UITableViewController {
2019-03-15 13:19:19 +01:00
2020-08-09 21:04:30 +02:00
@IBOutlet weak var infoButton : UIBarButtonItem !
2020-05-16 11:21:55 +02:00
private var classifier : Classifier ?
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
private var accessory : SearchAndDisplayAccessory ?
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
private var titleLabel : UILabel !
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
private var subtitleLabel : UILabel !
2019-03-15 13:19:19 +01:00
private var caps = [ Cap ] ( )
private var shownCaps = [ Cap ] ( )
2020-05-16 11:21:55 +02:00
private var matches : [ Int : Float ] ?
2019-03-15 13:19:19 +01:00
private var sortType : SortCriteria = . id
2020-05-16 11:21:55 +02:00
private var searchText : String ? = nil
2019-03-15 13:19:19 +01:00
private var sortAscending : Bool = false
// / T h i s w i l l b e s e t t o a c a p i d w h e n a d d i n g a c a p t o i t
2020-05-16 11:21:55 +02:00
private var capToAddImageTo : Int ?
2019-03-15 13:19:19 +01:00
2020-06-18 22:55:51 +02:00
private var isUnlocked = false
private var processingScreenHud : JGProgressHUD ?
2020-05-16 11:21:55 +02:00
// MARK: C o m p u t e d p r o p e r t i e s
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) "
}
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
private var subtitleText : String {
let capCount = app . database . capCount
2020-06-18 22:55:51 +02:00
guard capCount > 0 , isUnlocked else {
2020-05-16 11:21:55 +02:00
return " "
}
let allImages = app . database . imageCount
let ratio = Float ( allImages ) / Float ( capCount )
return String ( format : " %d images (%.2f per cap) " , allImages , ratio )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: O v e r r i d e s
override var inputAccessoryView : UIView ? {
get { return accessory }
}
override var canBecomeFirstResponder : Bool {
return true
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: - A c t i o n s
2020-08-09 21:04:30 +02:00
@IBAction func updateInfo ( _ sender : UIBarButtonItem , forEvent event : UIEvent ) {
guard let touch = event . allTouches ? . first , touch . tapCount > 0 , ! app . database . isInOfflineMode else {
showOfflineDialog ( )
return
}
2020-06-18 22:55:51 +02:00
downloadCapNames ( )
2020-05-16 11:21:55 +02:00
}
@IBAction func showMosaic ( _ sender : UIBarButtonItem ) {
2020-06-18 22:55:51 +02:00
checkThumbnailsAndColorsBeforShowingGrid ( )
2020-05-16 11:21:55 +02:00
}
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
2020-06-18 22:55:51 +02:00
controller . options = [ . id , . name ]
if isUnlocked { controller . options . append ( . count ) }
if matches != nil { controller . options . append ( . match ) }
2020-05-16 11:21:55 +02:00
let presentationController = AlwaysPresentAsPopover . configurePresentation ( forController : controller )
presentationController . sourceView = navigationItem . titleView !
presentationController . permittedArrowDirections = [ . up ]
self . present ( controller , animated : true )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: - L i f e c y c l e
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
override func viewDidLoad ( ) {
super . viewDidLoad ( )
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
tableView . rowHeight = 100
accessory = SearchAndDisplayAccessory ( width : self . view . frame . width )
accessory ? . delegate = self
2020-08-09 21:04:30 +02:00
initInfoButton ( )
2020-06-18 22:55:51 +02:00
app . database . delegate = self
let count = app . database . capCount
if count = = 0 {
log ( " No caps found, downloading names " )
downloadCapNames ( )
showProcessingScreen ( )
} else {
log ( " Loaded \( count ) caps " )
reloadCapsFromDatabase ( )
loadClassifier ( )
}
2020-05-16 11:21:55 +02:00
}
override func viewWillAppear ( _ animated : Bool ) {
super . viewWillAppear ( animated )
( navigationController as ? NavigationController ) ? . allowLandscape = false
2020-06-18 22:55:51 +02:00
isUnlocked = app . isUnlocked
log ( isUnlocked ? " App is unlocked " : " App is locked " )
2020-05-16 11:21:55 +02:00
}
override func didMove ( toParent parent : UIViewController ? ) {
super . didMove ( toParent : parent )
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
guard parent != nil && self . navigationItem . titleView = = nil else {
return
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
initNavigationItemTitleView ( )
}
2020-08-09 21:04:30 +02:00
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 )
}
2020-05-16 11:21:55 +02:00
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 )
2020-06-18 22:55:51 +02:00
let longPress = UILongPressGestureRecognizer ( target : self , action : #selector ( attemptChangeOfUserPermissions ) )
stackView . addGestureRecognizer ( longPress )
2020-05-16 11:21:55 +02:00
}
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
private func updateNavigationItemTitleView ( ) {
DispatchQueue . main . async {
self . titleLabel . text = self . titleText
self . subtitleLabel . text = self . subtitleText
}
}
// MARK: S t a r t i n g u p d a t e s
2020-06-18 22:55:51 +02:00
private func checkThumbnailsAndColorsBeforShowingGrid ( ) {
let missingImageCount = app . database . capCountWithoutImages
guard missingImageCount = = 0 else {
askUserToDownload ( capImages : missingImageCount )
return
}
createMissingThumbnailsBeforeShowingGrid ( )
}
private func createMissingThumbnailsBeforeShowingGrid ( ) {
let missing = app . database . capsWithoutThumbnails . map { $0 . id }
guard missing . count > 0 else {
log ( " No thumbnails missing, checking colors " )
checkColorsBeforeShowingGrid ( )
return
}
log ( " Generating \( missing . count ) thumbnails " )
let hud = JGProgressHUD ( style : traitCollection . userInterfaceStyle = = . dark ? . dark : . light )
hud . indicatorView = JGProgressHUDPieIndicatorView ( )
hud . detailTextLabel . text = " 0 % complete (0 / \( missing . count ) "
hud . textLabel . text = " Generating thumbnails "
hud . show ( in : self . view )
let group = DispatchGroup ( )
var done = 0
let split = 50
DispatchQueue . global ( qos : . background ) . async {
for part in missing . split ( intoPartsOf : split ) {
for id in part {
group . enter ( )
defer {
done += 1
let ratio = Float ( done ) / Float ( missing . count )
let percent = Int ( ( ratio * 100 ) . rounded ( ) )
DispatchQueue . main . async {
hud . progress = ratio
hud . detailTextLabel . text = " \( percent ) % complete ( \( done ) / \( missing . count ) ) "
}
group . leave ( )
}
guard let image = app . storage . image ( for : id ) else {
return
}
let thumbnail = Cap . thumbnail ( for : image )
app . storage . save ( thumbnail : thumbnail , for : id )
}
if group . wait ( timeout : . now ( ) + . seconds ( 30 ) ) != . success {
self . log ( " Timed out waiting for thumbnails to be generated " )
}
}
DispatchQueue . main . async {
hud . dismiss ( )
self . checkColorsBeforeShowingGrid ( )
}
}
}
private func checkColorsBeforeShowingGrid ( ) {
let missing = Array ( app . database . capsWithoutColors )
guard missing . count > 0 else {
log ( " No missing colors, showing grid " )
showGrid ( )
return
}
log ( " Generating \( missing . count ) colors " )
let hud = JGProgressHUD ( style : traitCollection . userInterfaceStyle = = . dark ? . dark : . light )
hud . indicatorView = JGProgressHUDPieIndicatorView ( )
hud . detailTextLabel . text = " 0 % complete (0 / \( missing . count ) "
hud . textLabel . text = " Generating colors "
hud . show ( in : self . view )
let group = DispatchGroup ( )
var done = 0
let split = 50
let context = CIContext ( options : [ . workingColorSpace : kCFNull ! ] )
DispatchQueue . global ( qos : . background ) . async {
for part in missing . split ( intoPartsOf : split ) {
for id in part {
group . enter ( )
defer {
done += 1
let ratio = Float ( done ) / Float ( missing . count )
let percent = Int ( ( ratio * 100 ) . rounded ( ) )
DispatchQueue . main . async {
hud . progress = ratio
hud . detailTextLabel . text = " \( percent ) % complete ( \( done ) / \( missing . count ) ) "
}
group . leave ( )
}
guard let image = app . storage . ciImage ( for : id ) else {
return
}
guard let color = image . averageColor ( context : context ) else {
return
}
app . database . set ( color : color , for : id )
}
if group . wait ( timeout : . now ( ) + . seconds ( 30 ) ) != . success {
self . log ( " Timed out waiting for colors to be generated " )
}
context . clearCaches ( )
}
DispatchQueue . main . async {
hud . dismiss ( )
self . 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
}
2020-08-09 21:04:30 +02:00
private func showOfflineDialog ( ) {
let offline = app . database . isInOfflineMode
if offline {
2020-08-19 19:25:17 +02:00
print ( " Marking as online " )
app . database . isInOfflineMode = false
app . database . uploadRemainingData ( )
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 " )
2020-08-09 21:04:30 +02:00
}
}
2020-06-18 22:55:51 +02:00
private func downloadCapNames ( ) {
app . database . downloadCapNames { success in
guard success else {
self . hideProcessingScreen ( )
self . showAlert ( " Failed to download cap names " , title : " Sync failed " )
return
}
self . downloadImageCounts ( )
}
}
2020-05-16 11:21:55 +02:00
private func downloadImageCounts ( ) {
2020-06-18 22:55:51 +02:00
app . database . downloadImageCounts { success in
guard success else {
self . hideProcessingScreen ( )
self . showAlert ( " Failed to download image counts " , title : " Sync failed " )
return
}
self . hideProcessingScreen ( )
self . checkIfCapImagesNeedDownload ( )
}
}
private func checkIfCapImagesNeedDownload ( ) {
let count = app . database . capCountWithoutImages
guard count > 0 else {
log ( " No cap images to download " )
self . downloadNewestClassifierIfNeeded ( )
return
}
DispatchQueue . main . async {
self . askUserToDownload ( capImages : count )
}
2020-05-16 11:21:55 +02:00
}
private func downloadNewestClassifierIfNeeded ( ) {
app . database . hasNewClassifier { version , size in
guard let version = version else {
2019-03-15 13:19:19 +01:00
return
}
2020-05-16 11:21:55 +02:00
DispatchQueue . main . async {
self . askUserToDownload ( classifier : version , size : size )
}
}
}
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 {
2020-06-18 22:55:51 +02:00
self . showAlert ( " Name could not be set. " , title : " Update failed " )
2020-05-16 11:21:55 +02:00
return
}
2019-03-15 13:19:19 +01:00
}
}
2020-05-16 11:21:55 +02:00
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
2020-06-18 22:55:51 +02:00
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 ( )
2020-05-16 11:21:55 +02:00
}
}
}
2019-03-15 13:19:19 +01:00
2020-06-18 22:55:51 +02:00
private func updateShownCaps ( _ newList : [ Cap ] , insertedId id : Int ) {
// M a i n q u e u e
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 ( )
}
2020-05-16 11:21:55 +02:00
// MARK: U s e r i n t e r a c t i o n
2020-06-18 22:55:51 +02:00
private func showProcessingScreen ( ) {
guard processingScreenHud = = nil else {
log ( " Already showing processing screen " )
2020-05-16 11:21:55 +02:00
return
}
2020-06-18 22:55:51 +02:00
let style : JGProgressHUDStyle = traitCollection . userInterfaceStyle = = . dark ? . dark : . extraLight
let hud = JGProgressHUD ( style : style )
hud . indicatorView = JGProgressHUDIndeterminateIndicatorView ( )
hud . detailTextLabel . text = " Please wait until the app has finished processing. "
hud . textLabel . text = " Processing... "
hud . show ( in : self . view )
self . processingScreenHud = hud
}
private func hideProcessingScreen ( ) {
processingScreenHud ? . dismiss ( )
processingScreenHud = nil
2020-05-16 11:21:55 +02:00
}
2020-06-18 22:55:51 +02:00
@objc private func attemptChangeOfUserPermissions ( ) {
guard isUnlocked else {
attemptAppUnlock ( )
2020-05-16 11:21:55 +02:00
return
}
2020-06-18 22:55:51 +02:00
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
2020-05-16 11:21:55 +02:00
}
2020-06-18 22:55:51 +02:00
self . unlockDidSucceed ( )
2020-05-16 11:21:55 +02:00
}
}
2020-06-18 22:55:51 +02:00
private func unlockFailed ( ) {
showAlert ( " The pin you entered is incorrect. " , title : " Invalid pin " )
2020-05-16 11:21:55 +02:00
}
2020-06-18 22:55:51 +02:00
private func unlockDidSucceed ( ) {
showAlert ( " The app was successfully unlocked. " , title : " Unlocked " )
isUnlocked = true
showAllCapsAndScrollToTop ( )
updateNavigationItemTitleView ( )
}
private func loadClassifier ( ) {
guard classifier = = nil else {
2020-05-16 11:21:55 +02:00
return
}
2020-06-18 22:55:51 +02:00
guard let model = app . storage . recognitionModel else {
downloadNewestClassifierIfNeeded ( )
return
2020-05-16 11:21:55 +02:00
}
2020-06-18 22:55:51 +02:00
classifier = Classifier ( model : model )
2020-05-16 11:21:55 +02:00
}
2020-06-18 22:55:51 +02:00
2020-05-16 11:21:55 +02:00
private func askUserToDownload ( capImages : Int ) {
let detail = " \( capImages ) caps have no image. Would you like to download them now? ( \( ByteCountFormatter . string ( fromByteCount : Int64 ( capImages * 10000 ) , countStyle : . file ) ) ) "
2020-06-18 22:55:51 +02:00
presentUserBinaryChoice ( " Download images " , detail : detail , yesText : " Download " , noText : " Later " , dismissed : {
self . downloadNewestClassifierIfNeeded ( )
} ) {
2020-05-16 11:21:55 +02:00
self . downloadAllCapImages ( )
}
2019-03-15 13:19:19 +01:00
}
2020-06-18 22:55:51 +02:00
2020-05-16 11:21:55 +02:00
private func askUserToDownload ( classifier version : Int , size : Int64 ? ) {
2020-06-18 22:55:51 +02:00
let oldVersion = app . database . classifierVersion
2020-05-16 11:21:55 +02:00
let sizeText = size != nil ? " ( \( ByteCountFormatter . string ( fromByteCount : size ! , countStyle : . file ) ) ) " : " "
2020-06-18 22:55:51 +02:00
guard oldVersion > 0 else {
askUserToDownloadFirst ( classifier : version , sizeText : sizeText )
return
}
askUserToDownloadNew ( classifier : version , sizeText : sizeText , oldVersion : oldVersion )
}
private func askUserToDownloadNew ( classifier version : Int , sizeText : String , oldVersion : Int ) {
let detail = " Version \( version ) of the classifier is available for download (You have version \( oldVersion ) ). Would you like to download it now? "
2020-05-16 11:21:55 +02:00
presentUserBinaryChoice ( " New classifier " , detail : detail + sizeText , yesText : " Download " ) {
self . downloadClassifier ( )
}
}
2020-06-18 22:55:51 +02:00
private func askUserToDownloadFirst ( classifier version : Int , sizeText : String ) {
let detail = " A classifier to match caps is available for download (version \( version ) . Would you like to download it now? "
presentUserBinaryChoice ( " Download classifier " , detail : detail + sizeText , yesText : " Download " ) {
self . downloadClassifier ( )
}
}
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 {
2020-05-16 11:21:55 +02:00
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 {
2019-03-15 13:19:19 +01:00
return
}
2020-05-16 11:21:55 +02:00
confirmed ( name )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
let cancel = UIAlertAction ( title : noText , style : . cancel )
alertController . addAction ( action )
alertController . addAction ( cancel )
self . present ( alertController , animated : true )
2019-03-15 13:19:19 +01:00
}
}
2020-05-16 11:21:55 +02:00
2020-06-18 22:55:51 +02:00
private func presentUserBinaryChoice ( _ title : String , detail : String , yesText : String , noText : String = " Cancel " , dismissed : ( ( ) -> Void ) ? = nil , confirmed : @ escaping ( ) -> Void ) {
2020-05-16 11:21:55 +02:00
let alert = UIAlertController ( title : title , message : detail , preferredStyle : . alert )
let confirm = UIAlertAction ( title : yesText , style : . default ) { _ in
confirmed ( )
}
2020-06-18 22:55:51 +02:00
let cancel = UIAlertAction ( title : noText , style : . cancel ) { _ in
dismissed ? ( )
}
2020-05-16 11:21:55 +02:00
alert . addAction ( confirm )
alert . addAction ( cancel )
self . present ( alert , animated : true )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: S t a r t i n g d o w n l o a d s
private func downloadClassifier ( ) {
2020-06-18 22:55:51 +02:00
let style : JGProgressHUDStyle = traitCollection . userInterfaceStyle = = . dark ? . dark : . light
let hud = JGProgressHUD ( style : style )
// h u d . v i b r a n c y E n a b l e d = t r u e
2020-05-16 11:21:55 +02:00
hud . indicatorView = JGProgressHUDPieIndicatorView ( )
hud . detailTextLabel . text = " 0 % complete "
2020-06-18 22:55:51 +02:00
hud . textLabel . text = " Downloading image classifier "
2020-05-16 11:21:55 +02:00
hud . show ( in : self . view )
app . database . downloadClassifier ( progress : { progress , received , total in
DispatchQueue . main . async {
hud . progress = progress
let t = ByteCountFormatter . string ( fromByteCount : total , countStyle : . file )
let r = ByteCountFormatter . string ( fromByteCount : received , countStyle : . file )
2020-06-18 22:55:51 +02:00
hud . detailTextLabel . text = String ( format : " %.0f " , progress * 100 ) + " % ( \( r ) / \( t ) ) "
2020-05-16 11:21:55 +02:00
}
} ) { success in
DispatchQueue . main . async {
hud . dismiss ( )
self . didDownloadClassifier ( successfully : success )
}
}
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
private func downloadAllCapImages ( ) {
2020-06-18 22:55:51 +02:00
let style : JGProgressHUDStyle = traitCollection . userInterfaceStyle = = . dark ? . dark : . light
let hud = JGProgressHUD ( style : style )
// h u d . v i b r a n c y E n a b l e d = t r u e
2020-05-16 11:21:55 +02:00
hud . indicatorView = JGProgressHUDPieIndicatorView ( )
hud . detailTextLabel . text = " 0 % complete "
hud . textLabel . text = " Downloading cap images "
hud . show ( in : self . view )
2020-06-18 22:55:51 +02:00
app . database . downloadMainCapImages { done , total in
2020-05-16 11:21:55 +02:00
let progress = Float ( done ) / Float ( total )
let percent = Int ( ( progress * 100 ) . rounded ( ) )
2020-06-18 22:55:51 +02:00
hud . detailTextLabel . text = " \( percent ) % ( \( done ) / \( total ) ) "
2020-05-16 11:21:55 +02:00
hud . progress = progress
if done >= total {
hud . dismiss ( afterDelay : 1.0 )
2020-06-18 22:55:51 +02:00
self . downloadNewestClassifierIfNeeded ( )
2020-05-16 11:21:55 +02:00
}
}
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: C l a s s i f i c a t i o n
// / T h e s i m i l a r i t y o f t h e c a p t o t h e c u r r e n t l y p r o c e s s e d i m a g e
private func match ( for cap : Int ) -> Float ? {
matches ? [ cap ]
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
private func clearClassifierMatches ( ) {
matches = nil
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
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 ( )
2020-06-18 22:55:51 +02:00
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 ) )
}
2020-05-16 11:21:55 +02:00
}
}
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: F i n i s h i n g d o w n l o a d s
private func didDownloadClassifier ( successfully success : Bool ) {
guard success else {
self . log ( " Failed to download classifier " )
return
}
2020-06-18 22:55:51 +02:00
loadClassifier ( )
2020-05-16 11:21:55 +02:00
self . log ( " Classifier was downloaded. " )
guard let image = accessory ! . currentImage else {
2020-06-18 22:55:51 +02:00
classifyDummyImage ( )
2020-05-16 11:21:55 +02:00
return
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
classify ( image : image )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: - S h o w i n g c a p s
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
private func reloadCapsFromDatabase ( ) {
caps = app . database ? . caps ? ? [ ]
showCaps ( )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
/* *
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 )
2019-03-15 13:19:19 +01:00
}
}
2020-05-16 11:21:55 +02:00
private func show ( caps : [ Cap ] ) {
show ( sortedCaps : sorted ( caps : caps ) )
}
private func show ( sortedCaps caps : [ Cap ] ) {
shownCaps = caps
DispatchQueue . main . async {
self . tableView . reloadData ( )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
}
private func filter ( caps : [ Cap ] , matching text : String ) -> [ Cap ] {
let textParts = text . components ( separatedBy : " " ) . filter { $0 != " " }
return caps . compactMap { cap -> Cap ? in
// F o r e a c h p a r t o f t e x t , c h e c k i f n a m e c o n t a i n s i t
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 }
2019-03-15 13:19:19 +01:00
}
}
}
2020-05-16 11:21:55 +02:00
// / R e s e t s t h e c a p l i s t t o i t s o r i g i n a l s t a t e , d i s c a r d i n g a n y p r e v i o u s s o r t i n g .
private func showAllCapsByDescendingId ( ) {
sortType = . id
sortAscending = false
showAllCapsAndScrollToTop ( )
}
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
// / D i s p l a y a l l c a p s i n t h e t a b l e , a n d s c r o l l s t o t h e t o p
private func showAllCapsAndScrollToTop ( ) {
showCaps ( )
tableViewScrollToTop ( )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: - T a b l e V i e w
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
/* *
Scroll the table view to the top
*/
private func tableViewScrollToTop ( ) {
2020-06-18 22:55:51 +02:00
guard shownCaps . count > 0 else { return }
2020-05-16 11:21:55 +02:00
let path = IndexPath ( row : 0 , section : 0 )
DispatchQueue . main . async {
self . tableView . scrollToRow ( at : path , at : . top , animated : true )
2019-03-15 13:19:19 +01:00
}
}
}
// MARK: - S o r t C o n t r o l l e r D e l e g a t e
extension TableView : SortControllerDelegate {
func sortController ( didSelect sortType : SortCriteria , ascending : Bool ) {
self . sortType = sortType
self . sortAscending = ascending
if sortType != . match {
2020-05-16 11:21:55 +02:00
clearClassifierMatches ( )
2019-03-15 13:19:19 +01:00
}
showAllCapsAndScrollToTop ( )
}
}
// MARK: - C a m e r a C o n t r o l l e r D e l e g a t e
extension TableView : CameraControllerDelegate {
func didCapture ( image : UIImage ) {
2020-05-16 11:21:55 +02:00
guard let cap = capToAddImageTo else {
2020-06-18 22:55:51 +02:00
accessory ! . showImageView ( with : image , isUnlocked : isUnlocked )
2020-05-16 11:21:55 +02:00
classify ( image : image )
return
}
guard app . database . add ( image : image , for : cap ) else {
self . error ( " Could not save image " )
return
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
log ( " Added image for cap \( cap ) " )
self . capToAddImageTo = nil
2019-03-15 13:19:19 +01:00
}
func didCancel ( ) {
capToAddImageTo = nil
}
}
// MARK: - U I T a b l e V i e w D a t a S o u r c e
2020-05-16 11:21:55 +02:00
extension TableView {
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
override func tableView ( _ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell {
2019-03-15 13:19:19 +01:00
let cell = tableView . dequeueReusableCell ( withIdentifier : " cap " ) as ! CapCell
let cap = shownCaps [ indexPath . row ]
2020-06-18 22:55:51 +02:00
configure ( cell : cell , for : cap )
2019-03-15 13:19:19 +01:00
return cell
}
2020-06-18 22:55:51 +02:00
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 = app . storage . image ( for : cap . id ) {
cell . set ( image : image )
} else {
cell . set ( image : nil )
app . database . downloadMainImage ( for : cap . id ) { _ in
// D e l e g a t e c a l l w i l l u p d a t e i m a g e
}
}
}
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
override func numberOfSections ( in tableView : UITableView ) -> Int {
2019-03-15 13:19:19 +01:00
return 1
}
2020-05-16 11:21:55 +02:00
override func tableView ( _ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int {
2019-03-15 13:19:19 +01:00
return shownCaps . count
}
}
// MARK: - U I T a b l e V i e w D e l e g a t e
2020-05-16 11:21:55 +02:00
extension TableView {
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
private func takeImage ( for cap : Int ) {
2019-03-15 13:19:19 +01:00
self . capToAddImageTo = cap
2020-05-16 11:21:55 +02:00
showCameraView ( )
2019-03-15 13:19:19 +01:00
}
2020-06-18 22:55:51 +02:00
override func tableView ( _ tableView : UITableView , willSelectRowAt indexPath : IndexPath ) -> IndexPath ? {
// P r e v e n t u n a u t h o r i z e d u s e r s f r o m s e l e c t i n g c a p s
isUnlocked ? indexPath : nil
}
2020-05-16 11:21:55 +02:00
override func tableView ( _ tableView : UITableView , didSelectRowAt indexPath : IndexPath ) {
defer {
tableView . deselectRow ( at : indexPath , animated : true )
}
2020-06-18 22:55:51 +02:00
// P r e v e n t u n a u t h o r i z e d u s e r s f r o m m a k i n g c h a n g e s
guard isUnlocked else {
return
}
2019-03-15 13:19:19 +01:00
let cap = shownCaps [ indexPath . row ]
2020-05-16 11:21:55 +02:00
guard let image = accessory ? . capImage . image else {
2019-03-15 13:19:19 +01:00
self . giveFeedback ( . medium )
2020-05-16 11:21:55 +02:00
takeImage ( for : cap . id )
return
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
guard app . database . add ( image : image , for : cap . id ) else {
self . giveFeedback ( . heavy )
self . error ( " Could not save image " )
return
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
self . giveFeedback ( . medium )
// D e l e g a t e c a l l w i l l u p d a t e c e l l
self . accessory ? . discardImage ( )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
2019-03-15 13:19:19 +01:00
private func giveFeedback ( _ style : UIImpactFeedbackGenerator . FeedbackStyle ) {
2020-06-18 22:55:51 +02:00
UIImpactFeedbackGenerator ( style : style ) . impactOccurred ( )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
override func tableView ( _ tableView : UITableView , trailingSwipeActionsConfigurationForRowAt indexPath : IndexPath ) -> UISwipeActionsConfiguration ? {
2020-06-18 22:55:51 +02:00
// P r e v e n t u n a u t h o r i z e d u s e r s f r o m m a k i n g c h a n g e s
guard isUnlocked else {
return nil
}
2019-03-15 13:19:19 +01:00
let cap = shownCaps [ indexPath . row ]
let rename = UIContextualAction ( style : . normal , title : " Rename \n cap " ) { ( _ , _ , success ) in
success ( true )
self . rename ( cap : cap , at : indexPath )
self . giveFeedback ( . medium )
}
rename . backgroundColor = . blue
let image = UIContextualAction ( style : . normal , title : " Change \n image " ) { ( _ , _ , success ) in
self . giveFeedback ( . medium )
let storyboard = UIStoryboard ( name : " Main " , bundle : nil )
let controller = storyboard . instantiateViewController ( withIdentifier : " ImageSelector " ) as ! ImageSelector
controller . cap = cap
self . navigationController ? . pushViewController ( controller , animated : true )
success ( true )
}
image . backgroundColor = . red
return UISwipeActionsConfiguration ( actions : [ rename , image ] )
}
2020-05-16 11:21:55 +02:00
override func tableView ( _ tableView : UITableView , leadingSwipeActionsConfigurationForRowAt indexPath : IndexPath ) -> UISwipeActionsConfiguration ? {
2019-03-15 13:19:19 +01:00
let cap = shownCaps [ indexPath . row ]
2020-06-18 22:55:51 +02:00
var actions = [ UIContextualAction ] ( )
// P r e v e n t u n a u t h o r i z e d u s e r s f r o m m a k i n g c h a n g e s
if isUnlocked {
let count = UIContextualAction ( style : . normal , title : " Update \n count " ) { ( _ , _ , success ) in
self . giveFeedback ( . medium )
success ( true )
DispatchQueue . global ( qos : . userInitiated ) . async {
app . database . downloadImageCount ( for : cap . id )
2020-05-16 11:21:55 +02:00
}
2019-03-15 13:19:19 +01:00
}
2020-06-18 22:55:51 +02:00
count . backgroundColor = . orange
actions . append ( count )
2019-03-15 13:19:19 +01:00
}
let similar = UIContextualAction ( style : . normal , title : " Similar \n caps " ) { ( _ , _ , success ) in
self . giveFeedback ( . medium )
2020-05-16 11:21:55 +02:00
self . accessory ? . hideImageView ( )
2020-06-18 22:55:51 +02:00
guard let image = app . storage . image ( for : cap . id ) else {
2020-05-16 11:21:55 +02:00
success ( false )
return
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
self . classify ( image : image )
2019-03-15 13:19:19 +01:00
success ( true )
}
similar . backgroundColor = . blue
2020-06-18 22:55:51 +02:00
actions . append ( similar )
2019-03-15 13:19:19 +01:00
2020-06-18 22:55:51 +02:00
return UISwipeActionsConfiguration ( actions : actions )
2019-03-15 13:19:19 +01:00
}
}
2020-05-16 11:21:55 +02:00
// MARK: - L o g g i n g
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
extension TableView : Logger { }
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
// MARK: - P r o t o c o l D a t a b a s e D e l e g a t e
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
extension TableView : DatabaseDelegate {
func database ( didAddCap cap : Cap ) {
caps . append ( cap )
updateNavigationItemTitleView ( )
guard let text = searchText else {
// A l l c a p s a r e s h o w n
let newList = sorted ( caps : caps )
updateShownCaps ( newList , insertedId : cap . id )
return
}
guard filter ( caps : [ cap ] , matching : text ) != [ ] else {
// C a p i s n o t s h o w n , s o d o n ' t r e l o a d
return
}
let newList = sorted ( caps : filter ( caps : caps , matching : text ) )
updateShownCaps ( newList , insertedId : cap . id )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
2020-06-18 22:55:51 +02:00
func database ( didChangeCap id : Int ) {
updateNavigationItemTitleView ( )
guard let cap = app . database . cap ( for : id ) else {
log ( " Changed cap \( id ) not found in database " )
2019-03-15 13:19:19 +01:00
return
}
2020-06-18 22:55:51 +02:00
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 {
2020-05-16 11:21:55 +02:00
return
}
2020-06-18 22:55:51 +02:00
configure ( cell : cell , for : cap )
}
func database ( didLoadImageForCap cap : Int ) {
guard let cell = visibleCell ( for : cap ) else {
return
}
guard let image = app . storage . image ( for : cap ) else {
log ( " No image for cap \( cap ) , although it should be loaded " )
return
2019-03-15 13:19:19 +01:00
}
2020-06-18 22:55:51 +02:00
cell . set ( image : image )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
2020-06-18 22:55:51 +02:00
func databaseNeedsFullRefresh ( ) {
2020-05-16 11:21:55 +02:00
reloadCapsFromDatabase ( )
}
2020-06-18 22:55:51 +02:00
private func visibleCell ( for cap : Int ) -> CapCell ? {
tableView . visibleCells
. map { $0 as ! CapCell }
. first { $0 . id = = cap }
}
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
// MARK: - P r o t o c o l C a p S e a r c h D e l e g a t e
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
extension TableView : CapAccessoryDelegate {
2019-03-15 13:19:19 +01:00
2020-05-16 11:21:55 +02:00
func capSearchWasDismissed ( ) {
showAllCapsAndScrollToTop ( )
}
func capSearch ( didChange text : String ) {
let cleaned = text . clean
guard cleaned != " " else {
self . showCaps ( matching : nil )
return
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
self . showCaps ( matching : cleaned )
2019-03-15 13:19:19 +01:00
}
2020-05-16 11:21:55 +02:00
func capAccessoryDidDiscardImage ( ) {
matches = nil
2019-03-15 13:19:19 +01:00
showAllCapsByDescendingId ( )
}
2020-05-16 11:21:55 +02:00
func capAccessory ( shouldSave image : UIImage ) {
saveNewCap ( for : image )
}
func capAccessoryCameraButtonPressed ( ) {
showCameraView ( )
}
2019-03-15 13:19:19 +01:00
}